@bitkyc08/opencodex 2.63.0-preview.20260923 → 2.64.0-preview.20260923

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 (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-D5eN1TID.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-B03pW-Uf.js → Tray-Bh_ErQDh.js} +1 -1
  10. package/gui/dist/assets/index-BEq4OCOz.js +86 -0
  11. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  12. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-EJxiUFMq.js +0 -50
  259. package/gui/dist/assets/index-BmJwNBHL.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -1,4 +1,5 @@
1
1
  import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
2
+ import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { readFileSync } from "node:fs";
4
5
  import type { CatalogModel } from "../../codex/catalog";
@@ -231,18 +232,22 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
231
232
  }).kind;
232
233
  if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(afterKind)) return;
233
234
  const routed = filterCatalogVisibleModels(allModels, current).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
235
+ const writtenProfile = current.claudeCode.desktopProfile;
236
+ const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
234
237
  const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
235
238
  current.port ?? 10100,
236
239
  [...desktopVisibleNativeSlugs(current)],
237
240
  routed,
238
241
  current.apiKeys?.[0]?.key,
239
242
  "static",
240
- current.claudeCode.desktopProfile,
243
+ writtenProfile,
241
244
  nativeContextLimits(current),
242
245
  );
243
246
  if (result.written && result.fingerprint) {
244
- current.claudeCode = { ...current.claudeCode, desktopProfile: { ...current.claudeCode.desktopProfile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
245
- saveConfigPreservingClaudeCode(current);
247
+ const marked = commitDesktopAppliedMarker(markerBaseline, result.fingerprint);
248
+ if (marked.status === "unavailable" || marked.value === false) {
249
+ console.warn("[claude-desktop] provider-change applied marker skipped");
250
+ }
246
251
  }
247
252
  } catch { /* best-effort */ }
248
253
  }
@@ -985,9 +990,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
985
990
  let body: { profile?: unknown };
986
991
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
987
992
  try {
988
- const { parseDesktopProfile, reconcileDesktopProfile } = await import("../../claude/desktop-profile");
993
+ const { parseDesktopProfile, preserveDesktopAppliedState, reconcileDesktopProfile, sameProfileContent } = await import("../../claude/desktop-profile");
989
994
  const parsed = parseDesktopProfile(body.profile);
990
- const current = await buildClaudeDesktopState(config);
995
+ const initial = loadConfig();
996
+ const current = await buildClaudeDesktopState(initial);
991
997
  const availableRoutes = new Set(current.models.filter(item => item.available).map(item => item.route));
992
998
  for (const route of Object.keys(parsed.assignments)) {
993
999
  if (!current.profile.assignments[route] && !availableRoutes.has(route)) {
@@ -1008,9 +1014,39 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1008
1014
  throw new Error(`현재 사용할 수 없는 모델은 기본값으로 지정할 수 없습니다: ${nextDefault}`);
1009
1015
  }
1010
1016
  }
1011
- const state = await buildClaudeDesktopState(config, parsed);
1012
- config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: reconcileDesktopProfile(state.profile, state.models) };
1013
- saveConfigPreservingClaudeCode(config);
1017
+ // Applied markers are server-owned bookkeeping. Discard client copies, then
1018
+ // restore the trusted markers only if the desired profile stayed identical.
1019
+ const editable = { version: 1 as const, assignments: parsed.assignments, defaults: parsed.defaults };
1020
+ const state = await buildClaudeDesktopState(initial, editable);
1021
+ const rebuilt = reconcileDesktopProfile(state.profile, state.models);
1022
+ const expectedProfile = initial.claudeCode?.desktopProfile;
1023
+ const outcome = mutatePersistedConfig<OcxConfig["claudeCode"] | null>(persisted => {
1024
+ const latest = persisted.claudeCode?.desktopProfile;
1025
+ if ((latest == null) !== (expectedProfile == null)
1026
+ || (latest && expectedProfile && !sameProfileContent(latest, expectedProfile))) {
1027
+ return { changed: false, value: null };
1028
+ }
1029
+ persisted.claudeCode = {
1030
+ ...(persisted.claudeCode ?? {}),
1031
+ desktopProfile: preserveDesktopAppliedState(latest ?? current.profile, rebuilt),
1032
+ };
1033
+ return { changed: true, value: structuredClone(persisted.claudeCode) };
1034
+ });
1035
+ if (outcome.status === "unavailable" && outcome.reason !== "conflict") {
1036
+ return jsonResponse({ error: "Claude Desktop profile could not be saved (config " + outcome.reason + ")" }, 500);
1037
+ }
1038
+ if (outcome.status === "unavailable" || outcome.value === null) {
1039
+ return jsonResponse({ error: "Claude Desktop profile changed during save" }, 409);
1040
+ }
1041
+ adoptPersistedClaudeCode(config, outcome.value);
1042
+ // An unarmed live snapshot may prefer its old marker during reconciliation.
1043
+ // Pin the field this route just committed while leaving other live leaves intact.
1044
+ if (outcome.value?.desktopProfile) {
1045
+ config.claudeCode = {
1046
+ ...(config.claudeCode ?? {}),
1047
+ desktopProfile: structuredClone(outcome.value.desktopProfile),
1048
+ };
1049
+ }
1014
1050
  const saved = await buildClaudeDesktopState(config);
1015
1051
  const runtimePort = Number(url.port) || config.port;
1016
1052
  return jsonResponse({ ok: true, ...saved, port: runtimePort });
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Management API for Anthropic OAuth usage-limit reset grants.
3
+ *
4
+ * GET /api/anthropic/reset-grants?accountId=... read one account's grants
5
+ * POST /api/anthropic/reset-grants/consume spend one grant
6
+ *
7
+ * Spending uses the user's own subscription benefit, so the POST requires the
8
+ * dashboard session principal (AGENTS.md, "User-consent actions"); the admin token
9
+ * alone is refused. Responses carry fixed codes and fixed messages only: no
10
+ * upstream body, exception text, token, organization id, or email. Nothing here
11
+ * logs. Lazy-loaded by `handleAnthropicResetGrantRoutesOnDemand` in
12
+ * management-api.ts so the core request path never imports it.
13
+ */
14
+ import { jsonResponse } from "../auth-cors";
15
+ import type { ManagementContext } from "./context";
16
+ import { getValidAccessSnapshotForAccount } from "../../oauth";
17
+ import { captureOAuthAccountSelection, listAccounts } from "../../oauth/store";
18
+ import {
19
+ ANTHROPIC_RESET_GRANT_ID_RE,
20
+ AnthropicResetGrantError,
21
+ AnthropicResetGrantUnknownOutcome,
22
+ anthropicResetGrantBlocker,
23
+ claimAnthropicResetGrant,
24
+ fetchAnthropicOrganizationUuid,
25
+ fetchAnthropicResetGrantStatus,
26
+ } from "../../providers/anthropic-reset-grants";
27
+ import {
28
+ AnthropicResetLedgerError,
29
+ anthropicOrgDigest,
30
+ anthropicResetOperationExists,
31
+ beginAnthropicResetOperation,
32
+ pendingAnthropicResetOperation,
33
+ releaseAnthropicResetLease,
34
+ settleAnthropicResetOperation,
35
+ } from "../../providers/anthropic-reset-grant-ledger";
36
+
37
+ const PROVIDER = "anthropic";
38
+ const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
39
+
40
+ const MESSAGES = {
41
+ method_not_allowed: "Method not allowed",
42
+ session_required: "Spending a reset requires a dashboard session.",
43
+ invalid_json: "Request body must be JSON.",
44
+ invalid_grant_id: "grantId is missing or malformed.",
45
+ invalid_operation_id: "operationId must be a UUIDv4.",
46
+ no_account: "No matching Anthropic OAuth account.",
47
+ auth_failed: "Sign in to this Anthropic account again.",
48
+ upstream_unavailable: "Anthropic did not return the reset-grant status.",
49
+ grant_not_usable: "This reset grant cannot be used right now.",
50
+ operation_identity_mismatch: "This operation belongs to another account or grant.",
51
+ in_flight: "This reset is still being processed.",
52
+ unresolved_prior_operation: "An earlier reset attempt for this grant is not confirmed yet.",
53
+ unknown_outcome_expired: "This attempt can no longer be retried; re-read the account.",
54
+ unknown_outcome: "The reset did not answer; its outcome is unknown.",
55
+ journal_write_failed: "The reset answered but could not be recorded; its outcome is unknown here.",
56
+ ledger_busy: "The reset journal is busy. Nothing was used.",
57
+ ledger_unavailable: "The reset journal is unavailable. Nothing was used.",
58
+ ledger_capacity: "The reset journal is full. Nothing was used.",
59
+ } as const;
60
+ type ErrorCode = keyof typeof MESSAGES;
61
+
62
+ export interface AnthropicResetGrantRouteDeps {
63
+ fetchFn?: typeof globalThis.fetch;
64
+ journalPath?: string;
65
+ now?: () => number;
66
+ listAccountIds: () => string[];
67
+ activeAccountId: () => string | null;
68
+ accessTokenFor: (accountId: string) => Promise<string>;
69
+ }
70
+
71
+ const defaultDeps: AnthropicResetGrantRouteDeps = {
72
+ listAccountIds: () => listAccounts(PROVIDER).map(account => account.id),
73
+ activeAccountId: () => captureOAuthAccountSelection(PROVIDER)?.accountId ?? null,
74
+ accessTokenFor: async accountId =>
75
+ (await getValidAccessSnapshotForAccount(PROVIDER, accountId, { requireUsableAccount: true })).accessToken,
76
+ };
77
+
78
+ function fail(ctx: ManagementContext, status: number, code: ErrorCode, extra: Record<string, unknown> = {}): Response {
79
+ return jsonResponse({ error: { code, message: MESSAGES[code], ...extra } }, status, ctx.req, ctx.config);
80
+ }
81
+
82
+ function ledgerFailure(ctx: ManagementContext, error: unknown): Response {
83
+ if (error instanceof AnthropicResetLedgerError && error.code === "busy") return fail(ctx, 503, "ledger_busy");
84
+ return fail(ctx, 503, "ledger_unavailable");
85
+ }
86
+
87
+ function readFailure(ctx: ManagementContext, error: unknown): Response {
88
+ if (error instanceof AnthropicResetGrantError && error.code === "auth") return fail(ctx, 401, "auth_failed");
89
+ return fail(ctx, 502, "upstream_unavailable");
90
+ }
91
+
92
+ function resolveAccountId(deps: AnthropicResetGrantRouteDeps, requested: unknown): string | null {
93
+ const ids = deps.listAccountIds();
94
+ if (typeof requested === "string" && requested.trim() !== "") {
95
+ return ids.includes(requested.trim()) ? requested.trim() : null;
96
+ }
97
+ const active = deps.activeAccountId();
98
+ return active && ids.includes(active) ? active : ids[0] ?? null;
99
+ }
100
+
101
+ async function tokenFor(deps: AnthropicResetGrantRouteDeps, accountId: string): Promise<string | null> {
102
+ try {
103
+ return await deps.accessTokenFor(accountId);
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ async function handleRead(ctx: ManagementContext, deps: AnthropicResetGrantRouteDeps): Promise<Response> {
110
+ const accountId = resolveAccountId(deps, ctx.url.searchParams.get("accountId") ?? undefined);
111
+ if (!accountId) return fail(ctx, 400, "no_account");
112
+ const accessToken = await tokenFor(deps, accountId);
113
+ if (!accessToken) return fail(ctx, 401, "auth_failed");
114
+ let status;
115
+ try {
116
+ status = await fetchAnthropicResetGrantStatus({ accessToken, fetchFn: deps.fetchFn });
117
+ } catch (error) {
118
+ return readFailure(ctx, error);
119
+ }
120
+ let pendingOperation = null;
121
+ let journalAvailable = true;
122
+ try {
123
+ pendingOperation = pendingAnthropicResetOperation(accountId, { journalPath: deps.journalPath, now: deps.now?.() });
124
+ } catch {
125
+ journalAvailable = false;
126
+ }
127
+ return jsonResponse({ accountId, ...status, pendingOperation, journalAvailable }, 200, ctx.req, ctx.config);
128
+ }
129
+
130
+ async function handleConsume(ctx: ManagementContext, deps: AnthropicResetGrantRouteDeps): Promise<Response> {
131
+ if (ctx.principal !== "gui-session") return fail(ctx, 403, "session_required");
132
+ let body: Record<string, unknown>;
133
+ try {
134
+ const parsed = await ctx.req.json() as unknown;
135
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return fail(ctx, 400, "invalid_json");
136
+ body = parsed as Record<string, unknown>;
137
+ } catch {
138
+ return fail(ctx, 400, "invalid_json");
139
+ }
140
+ const { grantId, operationId } = body;
141
+ if (typeof grantId !== "string" || !ANTHROPIC_RESET_GRANT_ID_RE.test(grantId)) return fail(ctx, 400, "invalid_grant_id");
142
+ if (typeof operationId !== "string" || !UUID_V4_RE.test(operationId)) return fail(ctx, 400, "invalid_operation_id");
143
+ if (typeof body.accountId !== "string") return fail(ctx, 400, "no_account");
144
+ const accountId = resolveAccountId(deps, body.accountId);
145
+ if (!accountId) return fail(ctx, 400, "no_account");
146
+
147
+ const accessToken = await tokenFor(deps, accountId);
148
+ if (!accessToken) return fail(ctx, 401, "auth_failed");
149
+ let organizationUuid: string;
150
+ try {
151
+ organizationUuid = await fetchAnthropicOrganizationUuid({ accessToken, fetchFn: deps.fetchFn });
152
+ } catch (error) {
153
+ return readFailure(ctx, error);
154
+ }
155
+ const ledger = { journalPath: deps.journalPath, now: deps.now?.() };
156
+
157
+ // A new operation must pass the spend gate on a fresh read. A same-id retry of
158
+ // an open record skips it: that attempt was already gated, and upstream
159
+ // answers a repeated request id for itself.
160
+ let known: boolean;
161
+ try {
162
+ known = anthropicResetOperationExists(operationId, ledger);
163
+ } catch (error) {
164
+ return ledgerFailure(ctx, error);
165
+ }
166
+ if (!known) {
167
+ let status;
168
+ try {
169
+ status = await fetchAnthropicResetGrantStatus({ accessToken, fetchFn: deps.fetchFn });
170
+ } catch (error) {
171
+ return readFailure(ctx, error);
172
+ }
173
+ const blocker = anthropicResetGrantBlocker(status, grantId);
174
+ if (blocker) return fail(ctx, 409, "grant_not_usable", { reason: blocker });
175
+ }
176
+
177
+ let begin;
178
+ try {
179
+ begin = beginAnthropicResetOperation(
180
+ { operationId, accountId, grantId, orgDigest: anthropicOrgDigest(organizationUuid) },
181
+ { journalPath: deps.journalPath, now: deps.now?.() },
182
+ );
183
+ } catch (error) {
184
+ return ledgerFailure(ctx, error);
185
+ }
186
+ switch (begin.kind) {
187
+ case "replay":
188
+ return jsonResponse(
189
+ { code: begin.code, replayed: true, resetsLeft: begin.resetsLeft, accountId, grantId, operationId },
190
+ 200, ctx.req, ctx.config,
191
+ );
192
+ case "identity-mismatch": return fail(ctx, 409, "operation_identity_mismatch");
193
+ case "in-flight": return fail(ctx, 409, "in_flight");
194
+ case "expired": return fail(ctx, 409, "unknown_outcome_expired");
195
+ case "unresolved-prior": return fail(ctx, 409, "unresolved_prior_operation", { pendingOperationId: begin.operationId });
196
+ case "capacity": return fail(ctx, 503, "ledger_capacity");
197
+ case "execute": break;
198
+ }
199
+
200
+ let answer;
201
+ try {
202
+ answer = await claimAnthropicResetGrant({
203
+ accessToken, organizationUuid, grantId, requestId: operationId, fetchFn: deps.fetchFn,
204
+ });
205
+ } catch {
206
+ try {
207
+ releaseAnthropicResetLease(operationId, { journalPath: deps.journalPath, now: deps.now?.() });
208
+ } catch { /* the lease expires on its own */ }
209
+ return fail(ctx, 502, "unknown_outcome", { operationId, grantId });
210
+ }
211
+ let settled;
212
+ try {
213
+ settled = settleAnthropicResetOperation(
214
+ { operationId, code: answer.code, resetsLeft: answer.resetsLeft },
215
+ { journalPath: deps.journalPath, now: deps.now?.() },
216
+ );
217
+ } catch {
218
+ // Fail closed: the caller must not read this as a durable result.
219
+ return fail(ctx, 500, "journal_write_failed", { operationId, grantId });
220
+ }
221
+ // Report what the journal holds. An overlapping same-id attempt may have
222
+ // settled first; its answer is the canonical one.
223
+ const replayed = settled.code !== answer.code || settled.resetsLeft !== answer.resetsLeft;
224
+ return jsonResponse(
225
+ {
226
+ code: settled.code,
227
+ replayed,
228
+ resetsLeft: settled.resetsLeft,
229
+ cleared: replayed ? [] : answer.cleared,
230
+ accountId,
231
+ grantId,
232
+ operationId,
233
+ },
234
+ 200, ctx.req, ctx.config,
235
+ );
236
+ }
237
+
238
+ export async function handleAnthropicResetGrantRoutes(
239
+ ctx: ManagementContext,
240
+ deps: AnthropicResetGrantRouteDeps = defaultDeps,
241
+ ): Promise<Response | null> {
242
+ const { pathname } = ctx.url;
243
+ if (pathname === "/api/anthropic/reset-grants") {
244
+ if (ctx.req.method !== "GET") return fail(ctx, 405, "method_not_allowed");
245
+ return handleRead(ctx, deps);
246
+ }
247
+ if (pathname === "/api/anthropic/reset-grants/consume") {
248
+ if (ctx.req.method !== "POST") return fail(ctx, 405, "method_not_allowed");
249
+ return handleConsume(ctx, deps);
250
+ }
251
+ return null;
252
+ }
@@ -403,7 +403,11 @@ export async function handleCodexPromptRoutes(ctx: ManagementContext): Promise<R
403
403
  }
404
404
  if (typeof body.title !== "string") return fail(ctx, "invalid_body", 400, "title must be a string");
405
405
  if (typeof body.body !== "string") return fail(ctx, "invalid_body", 400, "body must be a string");
406
- return settle(ctx, writeBaseVariant({ id, title: body.title, body: body.body }, revision, paths(ctx)));
406
+ const normalized = normalizeBody(body.body);
407
+ if (utf8Bytes(normalized) > MAX_BODY_BYTES) {
408
+ return fail(ctx, "body_too_large", 400, `base variant exceeds ${MAX_BODY_BYTES} bytes`);
409
+ }
410
+ return settle(ctx, writeBaseVariant({ id, title: body.title, body: normalized }, revision, paths(ctx)));
407
411
  }
408
412
 
409
413
  if (url.pathname === "/api/codex-prompt/adopt" && req.method === "POST") {
@@ -22,6 +22,7 @@ import {
22
22
  providerHeadersConfigError,
23
23
  saveConfigPreservingClaudeCode,
24
24
  } from "../../config";
25
+ import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
25
26
  import {
26
27
  clearLoginState,
27
28
  getLoginStatus,
@@ -219,18 +220,27 @@ export async function syncEnabledClientIntegrations(
219
220
  if (claudeDesktopIntegrationEnabled(latest)) {
220
221
  const routed = filterCatalogVisibleModels(models, latest)
221
222
  .map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
223
+ const writtenProfile = latest.claudeCode?.desktopProfile;
224
+ const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
222
225
  const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
223
226
  port,
224
227
  [...desktopVisibleNativeSlugs(latest)],
225
228
  routed,
226
229
  latest.apiKeys?.[0]?.key,
227
230
  "static",
228
- latest.claudeCode?.desktopProfile,
231
+ writtenProfile,
229
232
  nativeContextLimits(latest),
230
233
  );
231
- out.push(r.written
232
- ? { client: "claude-desktop", ok: true, changed: true }
233
- : { client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
234
+ if (!r.written || !r.fingerprint) {
235
+ out.push({ client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
236
+ } else {
237
+ const marked = commitDesktopAppliedMarker(markerBaseline, r.fingerprint);
238
+ out.push(marked.status === "unavailable"
239
+ ? { client: "claude-desktop", ok: false, reason: "Claude Desktop applied marker was not saved (" + marked.reason + ")" }
240
+ : marked.value === false
241
+ ? { client: "claude-desktop", ok: false, reason: "Claude Desktop desired profile changed during sync; applied marker skipped" }
242
+ : { client: "claude-desktop", ok: true, changed: true });
243
+ }
234
244
  }
235
245
  } catch (error) {
236
246
  out.push({ client: "claude-desktop", ok: false, reason: error instanceof Error ? error.message : String(error) });
@@ -147,8 +147,16 @@ function validateKeyName(
147
147
  return { value };
148
148
  }
149
149
 
150
+ function metaMuseConsentRequired(provider: string, principal: ManagementContext["principal"]): Response | null {
151
+ if (provider !== "meta-muse" || principal === "gui-session") return null;
152
+ return jsonResponse({
153
+ error: "Meta Muse login requires acknowledgement in the OpenCodex dashboard.",
154
+ code: "oauth_consent_required",
155
+ }, 403);
156
+ }
157
+
150
158
  export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
151
- const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
159
+ const { req, url, config, deps, principal, syncClaudeAgentDefsBestEffort } = ctx;
152
160
 
153
161
  if (url.pathname === "/api/accounts/events" && req.method === "GET") {
154
162
  const { accountSelectionStream } = await import("./account-selection-stream");
@@ -172,6 +180,12 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
172
180
  const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean; openBrowser?: unknown };
173
181
  const provider = (body.provider ?? "").trim().toLowerCase();
174
182
  if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
183
+ // Muse may import a local Keychain credential or start a device grant; add-account
184
+ // and reauth skip the import. All management login paths require the dashboard
185
+ // principal before credential acquisition. A raw token proves administration,
186
+ // not acknowledgement; caller-supplied headers are not consent evidence.
187
+ const consentRequired = metaMuseConsentRequired(provider, principal);
188
+ if (consentRequired) return consentRequired;
175
189
  const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
176
190
  if (namespaceCollision) return jsonResponse({ error: namespaceCollision }, 409);
177
191
  const accountId = body.accountId?.trim();
@@ -242,6 +256,8 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
242
256
  const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; input?: string; code?: string };
243
257
  const provider = (body.provider ?? "").trim().toLowerCase();
244
258
  if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
259
+ const consentRequired = metaMuseConsentRequired(provider, principal);
260
+ if (consentRequired) return consentRequired;
245
261
  const input = typeof body.input === "string" ? body.input : typeof body.code === "string" ? body.code : "";
246
262
  // Authorization responses are measured in hundreds of bytes; never accept the
247
263
  // generic management-body allowance here.
@@ -0,0 +1,164 @@
1
+ import { nonBlankStringArrayConfigError, normalizeNonBlankStringArray } from "../../config/provider-validation";
2
+ import type { OcxProviderConfig } from "../../types";
3
+
4
+ /**
5
+ * What a provider save keeps (#5563).
6
+ *
7
+ * `POST /api/providers` with an existing name replaces the stored row with a candidate built from
8
+ * the request. The dashboard form cannot send the fields below, so a save that omits them used
9
+ * to drop them from a custom provider, or reset them to the registry seed through
10
+ * `enrichProviderFromCatalog` for a registry provider. The live-config reconcile cannot restore
11
+ * them afterwards: the disk row equals its baseline, so the three-way merge keeps the value the
12
+ * save wrote.
13
+ *
14
+ * These five settings record how one upstream behaves. An overwrite that keeps the destination
15
+ * carries each one the request omits, including an explicit `[]` or `false`. An overwrite that
16
+ * moves the provider to another destination carries none of them: they describe the previous
17
+ * upstream, and registry enrichment already fills what is known about the new one. The same
18
+ * destination rule gates the stored `apiKeyPool`, whose keys were issued for the previous
19
+ * destination. A value the request sends always wins, and the rest of the old row is never merged
20
+ * into the candidate.
21
+ */
22
+ export const PROVIDER_COMPAT_CARRY_FIELDS = [
23
+ "preserveReasoningContentModels",
24
+ "requiresReasoningPlaceholderModels",
25
+ "foldDeveloperRoleToSystem",
26
+ "reasoningWireFormat",
27
+ "omitReasoningEffortWithToolsModels",
28
+ ] as const satisfies readonly (keyof OcxProviderConfig)[];
29
+
30
+ export type ProviderCompatCarryField = typeof PROVIDER_COMPAT_CARRY_FIELDS[number];
31
+
32
+ /** The only `reasoningWireFormat` value the adapters understand. */
33
+ export const PROVIDER_REASONING_WIRE_FORMATS: readonly NonNullable<OcxProviderConfig["reasoningWireFormat"]>[] = [
34
+ "gateway-object",
35
+ ];
36
+
37
+ /**
38
+ * Request ownership, sampled before `enrichProviderFromCatalog`. After enrichment "the client
39
+ * omitted this" and "the registry supplied it" look the same.
40
+ */
41
+ export interface ProviderOverwriteSample {
42
+ readonly submitted: ReadonlySet<ProviderCompatCarryField>;
43
+ readonly namesAuthMode: boolean;
44
+ }
45
+
46
+ export function sampleProviderOverwrite(provider: object): ProviderOverwriteSample {
47
+ return {
48
+ submitted: new Set(PROVIDER_COMPAT_CARRY_FIELDS.filter(field => Object.hasOwn(provider, field))),
49
+ namesAuthMode: Object.hasOwn(provider, "authMode"),
50
+ };
51
+ }
52
+
53
+ function normalizedDestinationUrl(value: unknown): string {
54
+ if (typeof value !== "string") return "";
55
+ const trimmed = value.trim();
56
+ try {
57
+ const url = new URL(trimmed);
58
+ return `${url.protocol}//${url.host}${url.pathname.replace(/\/+$/, "")}${url.search}`;
59
+ } catch {
60
+ return trimmed.replace(/\/+$/, "");
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Whether an overwrite keeps the provider on the same upstream: same adapter, same base URL
66
+ * (scheme and host compared case-insensitively, trailing slashes ignored) and, when the request
67
+ * names one, the same auth mode. The dashboard form sends `authMode` only for key and forward
68
+ * auth, so an omitted value is not evidence of a move; a stored row without `authMode` is a
69
+ * key-auth row.
70
+ */
71
+ export function providerOverwriteKeepsDestination(
72
+ candidate: Pick<OcxProviderConfig, "adapter" | "baseUrl" | "authMode">,
73
+ stored: Pick<OcxProviderConfig, "adapter" | "baseUrl" | "authMode"> | undefined,
74
+ sample: ProviderOverwriteSample,
75
+ ): boolean {
76
+ if (!stored) return false;
77
+ if ((candidate.adapter ?? "").trim() !== (stored.adapter ?? "").trim()) return false;
78
+ if (normalizedDestinationUrl(candidate.baseUrl) !== normalizedDestinationUrl(stored.baseUrl)) return false;
79
+ if (sample.namesAuthMode && (candidate.authMode ?? "key") !== (stored.authMode ?? "key")) return false;
80
+ return true;
81
+ }
82
+
83
+ /**
84
+ * Carry the stored compatibility settings the request omitted. `live` must be the row read after
85
+ * the route's DNS await, so a PATCH that saved one of them during the wait is kept.
86
+ */
87
+ export function carryProviderCompatFields(
88
+ candidate: OcxProviderConfig,
89
+ live: OcxProviderConfig | undefined,
90
+ sample: ProviderOverwriteSample,
91
+ ): void {
92
+ if (!live || !providerOverwriteKeepsDestination(candidate, live, sample)) return;
93
+ const target = candidate as unknown as Record<string, unknown>;
94
+ for (const field of PROVIDER_COMPAT_CARRY_FIELDS) {
95
+ if (sample.submitted.has(field)) continue;
96
+ const stored = live[field];
97
+ if (stored === undefined) continue;
98
+ target[field] = Array.isArray(stored) ? [...stored] : stored;
99
+ }
100
+ }
101
+
102
+ const REASONING_LIST_FIELDS = ["preserveReasoningContentModels", "requiresReasoningPlaceholderModels"] as const;
103
+
104
+ /**
105
+ * Type checks for a POST that does send one of the settings. `omitReasoningEffortWithToolsModels`
106
+ * is already checked by `providerManagementConfigError`.
107
+ */
108
+ export function providerCompatFieldConfigError(provider: Record<string, unknown>): string | null {
109
+ for (const field of REASONING_LIST_FIELDS) {
110
+ const error = nonBlankStringArrayConfigError(provider[field], field);
111
+ if (error) return error;
112
+ }
113
+ const fold = provider.foldDeveloperRoleToSystem;
114
+ if (fold !== undefined && typeof fold !== "boolean") return "foldDeveloperRoleToSystem must be a boolean";
115
+ const wire = provider.reasoningWireFormat;
116
+ if (wire !== undefined && !PROVIDER_REASONING_WIRE_FORMATS.includes(wire as never)) {
117
+ return `reasoningWireFormat must be one of: ${PROVIDER_REASONING_WIRE_FORMATS.join(", ")}`;
118
+ }
119
+ return null;
120
+ }
121
+
122
+ /**
123
+ * PATCH branches for the settings the field mask did not know. `null` clears a field. For the two
124
+ * reasoning lists an empty array is kept rather than deleted: it is the explicit opt-out that stops
125
+ * the registry seed from filling the field back in (see the note under OAUTH_RECONCILE_FIELDS in
126
+ * src/oauth/index.ts).
127
+ */
128
+ export function applyProviderCompatPatchFields(
129
+ rawBody: Record<string, unknown>,
130
+ next: OcxProviderConfig,
131
+ ): { touched: boolean } | { error: string } {
132
+ let touched = false;
133
+ for (const field of REASONING_LIST_FIELDS) {
134
+ if (!Object.hasOwn(rawBody, field)) continue;
135
+ const value = rawBody[field];
136
+ if (value === null) {
137
+ delete next[field];
138
+ } else {
139
+ const error = nonBlankStringArrayConfigError(value, field);
140
+ if (error) return { error };
141
+ next[field] = normalizeNonBlankStringArray(value as string[]);
142
+ }
143
+ touched = true;
144
+ }
145
+ if (Object.hasOwn(rawBody, "foldDeveloperRoleToSystem")) {
146
+ const value = rawBody.foldDeveloperRoleToSystem;
147
+ if (value === null) delete next.foldDeveloperRoleToSystem;
148
+ else if (typeof value === "boolean") next.foldDeveloperRoleToSystem = value;
149
+ else return { error: "foldDeveloperRoleToSystem must be a boolean" };
150
+ touched = true;
151
+ }
152
+ if (Object.hasOwn(rawBody, "reasoningWireFormat")) {
153
+ const value = rawBody.reasoningWireFormat;
154
+ if (value === null) {
155
+ delete next.reasoningWireFormat;
156
+ } else {
157
+ const format = PROVIDER_REASONING_WIRE_FORMATS.find(candidate => candidate === value);
158
+ if (!format) return { error: `reasoningWireFormat must be one of: ${PROVIDER_REASONING_WIRE_FORMATS.join(", ")}` };
159
+ next.reasoningWireFormat = format;
160
+ }
161
+ touched = true;
162
+ }
163
+ return { touched };
164
+ }