@bitkyc08/opencodex 2.55.0 → 2.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -313,7 +313,18 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
313
313
  // here keeps that adjacency intact.
314
314
  const reasoningText = assistantReasoningText(msg);
315
315
  if (reasoningText !== undefined) {
316
- input.push({ type: "reasoning", content: [{ type: "reasoning_text", text: reasoningText }] });
316
+ // `summary` is required on a reasoning input item by the OpenAI Responses API, and our
317
+ // own responsesRequestSchema marks it optional, so a summary-less item validated locally
318
+ // and was refused upstream with `Missing required parameter: 'input[N].summary'`. It also
319
+ // has to carry the text, not just satisfy the field: sanitizeReasoningInputContent blanks
320
+ // `content` for every destination except a `preserveResponsesReasoningContent` provider,
321
+ // so summary is the only channel that survives to a native backend. This mirrors the
322
+ // Claude ingress (src/claude/inbound.ts), which has always minted both.
323
+ input.push({
324
+ type: "reasoning",
325
+ summary: [{ type: "summary_text", text: reasoningText }],
326
+ content: [{ type: "reasoning_text", text: reasoningText }],
327
+ });
317
328
  }
318
329
  const blocks = assistantContentToBlocks(msg.content);
319
330
  if (blocks.length > 0) input.push({ type: "message", role: "assistant", content: blocks });
@@ -22,8 +22,34 @@ export interface RenderedDesktopModel extends DesktopProfileModel {
22
22
  supports1m: boolean;
23
23
  }
24
24
 
25
- const DATE_ALIAS = /^claude-opus-4-8-(2026\d{4})$/;
26
- const DAY_COUNT_2026 = 365;
25
+ // Managed-namespace date aliases run 2026-2035. The original 2026-only
26
+ // (365 slots) design failed with "all 365 encoded date slots are occupied"
27
+ // once a catalog exceeded 365 routes (stale assignments are retained by
28
+ // design, so the set only grows). Years before 2026 stay rejected: dated
29
+ // ids like `claude-opus-4-8-20250201` are real model snapshot ids, not
30
+ // managed aliases, and the inbound decoder relies on that distinction.
31
+ // Every emitted suffix stays 8 digits so modelMap date-stripping keeps
32
+ // working.
33
+ const DATE_ALIAS = /^claude-opus-4-8-(202[6-9]\d{4}|203[0-5]\d{4})$/;
34
+ const LEGACY_YEAR = 2026;
35
+ const LEGACY_DAY_COUNT = 365;
36
+ const ALIAS_FIRST_YEAR = 2026;
37
+ const ALIAS_LAST_YEAR = 2035;
38
+
39
+ function isLeapYear(year: number): boolean {
40
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
41
+ }
42
+
43
+ function daysInAliasYear(year: number): number {
44
+ return isLeapYear(year) ? 366 : 365;
45
+ }
46
+
47
+ export const TOTAL_ALIAS_SLOTS = (() => {
48
+ let total = 0;
49
+ for (let year = ALIAS_FIRST_YEAR; year <= ALIAS_LAST_YEAR; year += 1) total += daysInAliasYear(year);
50
+ return total;
51
+ })();
52
+ export const ALIAS_YEAR_RANGE = { first: ALIAS_FIRST_YEAR, last: ALIAS_LAST_YEAR } as const;
27
53
 
28
54
  export class DesktopProfileError extends Error {
29
55
  constructor(message: string, readonly path = "profile") {
@@ -119,7 +145,7 @@ export function parseDesktopProfile(value: unknown): DesktopProfile {
119
145
  if (isRealAnthropicRoute(route)) {
120
146
  if (raw.alias !== routeModelId(route)) throw new DesktopProfileError("real Anthropic routes must keep their exact model id", `profile.assignments.${route}.alias`);
121
147
  } else if (!validDateAlias(raw.alias)) {
122
- throw new DesktopProfileError("must be a valid claude-opus-4-8-2026MMDD alias", `profile.assignments.${route}.alias`);
148
+ throw new DesktopProfileError("must be a valid claude-opus-4-8-YYYYMMDD alias", `profile.assignments.${route}.alias`);
123
149
  }
124
150
  if (aliases.has(raw.alias)) throw new DesktopProfileError(`duplicate alias "${raw.alias}"`, `profile.assignments.${route}.alias`);
125
151
  aliases.add(raw.alias);
@@ -144,26 +170,57 @@ export function parseDesktopProfile(value: unknown): DesktopProfile {
144
170
  return { version: 1, assignments, defaults, ...appliedMarkers(value) };
145
171
  }
146
172
 
147
- function dayOfYearAlias(dayIndex: number): string {
148
- const date = new Date(Date.UTC(2026, 0, dayIndex + 1));
173
+ function formatSlotDate(year: number, dayOfYear: number): string {
174
+ const date = new Date(Date.UTC(year, 0, dayOfYear));
149
175
  const y = date.getUTCFullYear();
150
176
  const m = String(date.getUTCMonth() + 1).padStart(2, "0");
151
177
  const d = String(date.getUTCDate()).padStart(2, "0");
152
178
  return `claude-opus-4-8-${y}${m}${d}`;
153
179
  }
154
180
 
181
+ // Legacy 2026 ring, byte-identical to the original allocator: the same route
182
+ // must keep resolving to the same 2026 alias it always had, and a probe over
183
+ // a nearly-full 2026 set must land on the same free date as before.
184
+ function legacyDayAlias(dayIndex: number): string {
185
+ return formatSlotDate(LEGACY_YEAR, dayIndex + 1);
186
+ }
187
+
188
+ // Overflow ring for catalogs past 365 routes (2027-2035). Probed only after
189
+ // every legacy slot is taken, so existing profiles never shift into it.
190
+ const OVERFLOW_FIRST_YEAR = LEGACY_YEAR + 1;
191
+ const OVERFLOW_SLOT_COUNT = TOTAL_ALIAS_SLOTS - LEGACY_DAY_COUNT;
192
+
193
+ function overflowSlotAlias(slotIndex: number): string {
194
+ let remaining = ((slotIndex % OVERFLOW_SLOT_COUNT) + OVERFLOW_SLOT_COUNT) % OVERFLOW_SLOT_COUNT;
195
+ for (let year = OVERFLOW_FIRST_YEAR; year <= ALIAS_LAST_YEAR; year += 1) {
196
+ const days = daysInAliasYear(year);
197
+ if (remaining < days) return formatSlotDate(year, remaining + 1);
198
+ remaining -= days;
199
+ }
200
+ throw new DesktopProfileError("slot index out of range", "profile.assignments");
201
+ }
202
+
155
203
  function routeStartDay(route: string): number {
156
- return createHash("sha256").update(route).digest().readUInt32BE(0) % DAY_COUNT_2026;
204
+ return createHash("sha256").update(route).digest().readUInt32BE(0) % LEGACY_DAY_COUNT;
205
+ }
206
+
207
+ function routeOverflowStart(route: string): number {
208
+ return createHash("sha256").update(route).digest().readUInt32BE(4) % OVERFLOW_SLOT_COUNT;
157
209
  }
158
210
 
159
211
  function allocateAlias(route: string, used: Set<string>): string {
160
212
  if (isRealAnthropicRoute(route)) return routeModelId(route);
161
213
  const start = routeStartDay(route);
162
- for (let offset = 0; offset < DAY_COUNT_2026; offset += 1) {
163
- const alias = dayOfYearAlias((start + offset) % DAY_COUNT_2026);
214
+ for (let offset = 0; offset < LEGACY_DAY_COUNT; offset += 1) {
215
+ const alias = legacyDayAlias((start + offset) % LEGACY_DAY_COUNT);
216
+ if (!used.has(alias)) return alias;
217
+ }
218
+ const overflowStart = routeOverflowStart(route);
219
+ for (let offset = 0; offset < OVERFLOW_SLOT_COUNT; offset += 1) {
220
+ const alias = overflowSlotAlias((overflowStart + offset) % OVERFLOW_SLOT_COUNT);
164
221
  if (!used.has(alias)) return alias;
165
222
  }
166
- throw new DesktopProfileError("all 365 encoded date slots are occupied", `profile.assignments.${route}.alias`);
223
+ throw new DesktopProfileError(`all ${TOTAL_ALIAS_SLOTS} encoded date slots are occupied`, `profile.assignments.${route}.alias`);
167
224
  }
168
225
 
169
226
  export function reconcileDesktopProfile(
@@ -212,6 +212,8 @@ interface OpenBlock {
212
212
  argsBuf?: string;
213
213
  argsBufBytes?: number;
214
214
  webSearchArgsEmitted?: boolean;
215
+ /** True once ordinary function-call arguments were emitted to Anthropic SSE. */
216
+ toolArgsEmitted?: boolean;
215
217
  callId?: string;
216
218
  /** Last fixed-size reasoning identity (item + summary/content index) seen by this block. */
217
219
  reasoningPartKey?: string;
@@ -488,6 +490,7 @@ export function responsesSseToAnthropicSse(
488
490
  argsBuf: "",
489
491
  argsBufBytes: 0,
490
492
  webSearchArgsEmitted: false,
493
+ toolArgsEmitted: false,
491
494
  };
492
495
  break;
493
496
  }
@@ -518,6 +521,14 @@ export function responsesSseToAnthropicSse(
518
521
  type: "content_block_delta", index: open.index,
519
522
  delta: { type: "input_json_delta", partial_json: data.delta },
520
523
  });
524
+ open.toolArgsEmitted = true;
525
+ break;
526
+ }
527
+ case "response.function_call_arguments.done": {
528
+ if (!open || open.kind !== "tool_use" || open.bufferWebSearchArgs || open.toolArgsEmitted) break;
529
+ if (typeof data.arguments !== "string" || data.arguments.length === 0) break;
530
+ emit("content_block_delta", { type: "content_block_delta", index: open.index, delta: { type: "input_json_delta", partial_json: data.arguments } });
531
+ open.toolArgsEmitted = true;
521
532
  break;
522
533
  }
523
534
  case "response.output_item.done": {
@@ -565,6 +576,13 @@ export function responsesSseToAnthropicSse(
565
576
  delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
566
577
  });
567
578
  open.webSearchArgsEmitted = true;
579
+ } else if (!open.bufferWebSearchArgs && !open.toolArgsEmitted
580
+ && typeof item.arguments === "string" && item.arguments.length > 0) {
581
+ emit("content_block_delta", {
582
+ type: "content_block_delta", index: open.index,
583
+ delta: { type: "input_json_delta", partial_json: item.arguments },
584
+ });
585
+ open.toolArgsEmitted = true;
568
586
  }
569
587
  closeOpenBlock();
570
588
  }
@@ -245,7 +245,7 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
245
245
  }
246
246
  if (noWait) {
247
247
  printStatus({ flowId: startFlowId, ...pending });
248
- console.log("follow up: ocx account main reauth status --flow " + startFlowId);
248
+ if (!wantsJson) console.log("follow up: ocx account main reauth status --flow " + startFlowId);
249
249
  return 0;
250
250
  }
251
251
  // Blocking wait bounded by the service flow expiry (15-minute grant + margin).
@@ -710,10 +710,10 @@ export const CAPABILITIES: readonly Capability[] = [
710
710
  },
711
711
  {
712
712
  command: ["system", "codex-restart"],
713
- summary: "Restart the Codex app-server.",
713
+ summary: "Restart the Codex desktop app and app-servers.",
714
714
  routes: [{ method: "POST", path: "/api/system/codex-restart" }],
715
715
  flags: [
716
- { name: "--yes", value: "boolean", summary: "Required: restarts the operator's running Codex app-server." },
716
+ { name: "--yes", value: "boolean", summary: "Required: fully quits and relaunches the operator's Codex desktop app and restarts its app-servers." },
717
717
  { name: "--json", value: "boolean", summary: "Emit the restart result as JSON." },
718
718
  ],
719
719
  mutates: true,
package/src/cli/combo.ts CHANGED
@@ -15,7 +15,8 @@ const USAGE = `Usage:
15
15
  ocx combo show <id> [--json]
16
16
  ocx combo set <id> --targets <provider/model[:weight],...>
17
17
  [--strategy <failover|round-robin|random|least-used|reset-window>] [--sticky <1-100>]
18
- [--effort <low|medium|high|xhigh|max|ultra|->] [--alias <name|->]
18
+ [--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force>]
19
+ (force overrides valid client effort and can increase cost/latency) [--alias <name|->]
19
20
  [--native-alias] [--display-name <label|->]
20
21
  [--rename-from <id>] [--json]
21
22
  ocx combo remove <id> --yes [--json]`;
@@ -80,6 +81,10 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
80
81
  if (strategy !== "round-robin") throw new CliUsageError("--sticky applies only to round-robin", USAGE);
81
82
  }
82
83
  const effort = takeOption(args, "--effort");
84
+ const effortMode = takeOption(args, "--effort-mode");
85
+ if (effortMode !== undefined && effortMode !== "fallback" && effortMode !== "force") {
86
+ throw new CliUsageError("--effort-mode must be fallback or force", USAGE);
87
+ }
83
88
  const alias = takeOption(args, "--alias");
84
89
  const nativeAlias = takeFlag(args, "--native-alias");
85
90
  const displayName = takeOption(args, "--display-name");
@@ -91,12 +96,16 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
91
96
  targets: parseTargets(targetsRaw),
92
97
  };
93
98
  if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort;
99
+ if (effortMode !== undefined) combo.defaultEffortMode = effortMode;
94
100
  if (alias !== undefined) combo.alias = alias === "-" ? "" : alias;
95
101
  if (nativeAlias) combo.nativeAlias = true;
96
102
  if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName;
97
103
  const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
98
104
  const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id));
99
105
  if (existing?.imageInput === "disabled") combo.imageInput = "disabled";
106
+ if (effortMode === undefined && existing?.defaultEffortMode === "force") {
107
+ combo.defaultEffortMode = effort === "-" ? "fallback" : "force";
108
+ }
100
109
  const result = await runtimeRequest("/api/combos", {
101
110
  method: "PUT",
102
111
  body: JSON.stringify({ id, combo, ...(renameFrom ? { renameFrom } : {}) }),
package/src/cli/index.ts CHANGED
@@ -15,7 +15,7 @@ try {
15
15
  }
16
16
  import { currentExternalCodexModelProvider, restoreNativeCodex, restoreNativeCodexAsync, shouldInjectApiAuthHeader } from "../codex/inject";
17
17
  import { stripGrokConfig } from "../grok/inject";
18
- import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../update/stop-contract.mjs";
18
+ import { STOP_HISTORY_DEFERRED_EXIT_CODE, STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../update/stop-contract.mjs";
19
19
  import {
20
20
  describeHistoryJobFailure,
21
21
  resolveCodexHistoryJobTarget,
@@ -46,6 +46,7 @@ import {
46
46
  isPendingTeardownAbandoned,
47
47
  listPendingTeardowns,
48
48
  pendingTeardownPathFor,
49
+ pendingTeardownsAreExactly,
49
50
  quarantinePendingTeardown,
50
51
  } from "../config/pending-teardown";
51
52
  import { collectStatus, hubStatusLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
@@ -785,9 +786,16 @@ async function handleRestartStartWhenStopped(): Promise<boolean | "skipped"> {
785
786
  *
786
787
  * The distinction exists because `ocx update` must proceed for the first and abort for the
787
788
  * second, and it can only see an exit code (#3008).
789
+ *
790
+ * `historyDeferred` is the third kind (#4718). The Codex history preflight refuses BEFORE
791
+ * the config half runs, so nothing was restored at all: config, catalog, history and
792
+ * provenance are untouched and the client is still routed at the proxy that just stopped.
793
+ * Like `historyOnly` the proxy is genuinely down, so an update may replace package files.
794
+ * Unlike `historyOnly` the obligation was not performed, so the receipt must survive.
788
795
  */
789
- async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boolean; other: boolean }> {
796
+ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boolean; historyDeferred: boolean; other: boolean }> {
790
797
  let historyOnly = false;
798
+ let historyDeferred = false;
791
799
  let other = false;
792
800
  try {
793
801
  const result = await restoreNativeCodexAsync();
@@ -798,7 +806,16 @@ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boole
798
806
  // not — a client reads those, so their failure is a real teardown failure.
799
807
  const artifacts = result.artifacts;
800
808
  const configOrCatalogFailed = artifacts.config.state === "failed" || artifacts.catalog.state === "failed";
801
- if (!configOrCatalogFailed && artifacts.history.state === "failed") historyOnly = true;
809
+ // A preflight refusal reports every artifact as `skipped` because none of them were
810
+ // attempted. Reading the states alone cannot tell that apart from an ownership
811
+ // refusal, so the structured reason carries it and the states are still required to
812
+ // agree — a refusal that somehow reports a failed artifact is not this case.
813
+ const preflightRefused = result.historyPreflightRefusal !== undefined
814
+ && artifacts.config.state === "skipped"
815
+ && artifacts.catalog.state === "skipped"
816
+ && artifacts.history.state === "skipped";
817
+ if (preflightRefused) historyDeferred = true;
818
+ else if (!configOrCatalogFailed && artifacts.history.state === "failed") historyOnly = true;
802
819
  else other = true;
803
820
  console.error(`⚠️ ${result.message}`);
804
821
  }
@@ -816,7 +833,7 @@ async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boole
816
833
  other = true;
817
834
  console.error(`⚠️ Grok config restore failed: ${error instanceof Error ? error.message : String(error)}`);
818
835
  }
819
- return { historyOnly, other };
836
+ return { historyOnly, historyDeferred, other };
820
837
  }
821
838
 
822
839
  async function handleStop() {
@@ -860,6 +877,11 @@ async function handleStop() {
860
877
  };
861
878
  let stopFailed = false;
862
879
  let historyOnlyFailure = false;
880
+ /**
881
+ * Obligations this run deliberately kept because the Codex history preflight refused
882
+ * before restoring anything (#4718). Non-null selects the deferred exit code.
883
+ */
884
+ let historyDeferredNonces: string[] | null = null;
863
885
  // Only Task Scheduler respawns after a successful stop (#764), so only it earns the
864
886
  // restart-window wait; launchd, systemd and WinSW are down when they say so.
865
887
  let schedulerCanRespawn = false;
@@ -1138,6 +1160,7 @@ async function handleStop() {
1138
1160
  }
1139
1161
  const restore = await restoreSharedClientStateAfterStop();
1140
1162
  if (restore.other) stopFailed = true;
1163
+ else if (restore.historyDeferred) historyDeferredNonces = teardownNonce ? [teardownNonce, ...recoveredNonces] : recoveredNonces;
1141
1164
  else if (restore.historyOnly) historyOnlyFailure = true;
1142
1165
  // The obligation is discharged whether or not history metadata finalized: config and
1143
1166
  // catalog are what a client reads, and `restore.other` already fails the stop.
@@ -1145,7 +1168,16 @@ async function handleStop() {
1145
1168
  // Each nonce names its own file, so a clear can only ever remove the obligation it
1146
1169
  // names — never one a concurrent stop wrote. Both this run's claim and every inherited
1147
1170
  // receipt it proved discharged are released together.
1148
- if (!restore.other) {
1171
+ //
1172
+ // A history-preflight refusal is the exception: it restored nothing, so there is
1173
+ // nothing to discharge. Clearing here would drop a real obligation on the floor and
1174
+ // leave the client config pointing at a proxy that is gone, with nothing on disk
1175
+ // saying so — which is the whole failure the receipt exists to prevent (#4718).
1176
+ if (restore.historyDeferred) {
1177
+ console.error(" The shared teardown was refused before it changed anything, so it is still owed.");
1178
+ console.error(" Its receipt is preserved; run 'ocx stop' again once Codex is closed to retry the restore.");
1179
+ }
1180
+ if (!restore.other && !restore.historyDeferred) {
1149
1181
  const discharged = teardownNonce ? [teardownNonce, ...recoveredNonces] : recoveredNonces;
1150
1182
  for (const nonce of discharged) {
1151
1183
  // A receipt that survives its discharge re-triggers recovery forever, so a failed
@@ -1185,6 +1217,17 @@ async function handleStop() {
1185
1217
  // still wins: it is the stronger signal.
1186
1218
  if (stopFailed) process.exitCode = 1;
1187
1219
  else if (historyOnlyFailure) process.exitCode = STOP_HISTORY_INCOMPLETE_EXIT_CODE;
1220
+ // The deferred code says "the only obligations left are the ones I just decided to
1221
+ // keep". It is read across a process boundary by an updater that will replace package
1222
+ // files on the strength of it, so this run has to be able to prove the claim: if any
1223
+ // other obligation is sitting in the home — quarantined, or a concurrent stop's — the
1224
+ // claim is false and the ordinary failure code is the honest answer. That is also the
1225
+ // behaviour before #4718, so the fallback loses nothing that used to work.
1226
+ else if (historyDeferredNonces) {
1227
+ process.exitCode = pendingTeardownsAreExactly(historyDeferredNonces)
1228
+ ? STOP_HISTORY_DEFERRED_EXIT_CODE
1229
+ : 1;
1230
+ }
1188
1231
  return !stopFailed;
1189
1232
  }
1190
1233
 
@@ -407,7 +407,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
407
407
  "Ensures the proxy is running, then execs `claude` with ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN,",
408
408
  "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 and model slots from config.claudeCode.",
409
409
  "When Claude routing is explicitly disabled, it launches natively after removing proven OpenCodex-owned proxy state.",
410
- "Routed models appear in the native /model picker with stable claude-opus-4-8-2026MMDD slot aliases (Claude Code >= 2.1.129).",
410
+ "Routed models appear in the native /model picker with stable claude-opus-4-8-YYYYMMDD slot aliases,",
411
+ "where the year runs 2026-2035 and 2026 slots are allocated first (Claude Code >= 2.1.129).",
411
412
  "Older versions: pick models via ANTHROPIC_MODEL or /model <id> directly (any string passes through).",
412
413
  "User-exported ANTHROPIC_* variables take precedence for routed launches; native fallback removes only proven OpenCodex-owned proxy values.",
413
414
  "",
@@ -130,14 +130,14 @@ export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps =
130
130
  const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
131
131
  printData(await runtimeRequest("/api/system/codex-app-server", {}, deps), wantsJson);
132
132
  } else if (sub === "codex-restart") {
133
- // --yes required: this restarts the user's running Codex app-server, so it is exactly the
134
- // class of action that must not happen because an agent guessed a subcommand.
133
+ // --yes required: this fully quits and relaunches the user's Codex desktop app as well as
134
+ // restarting app-servers; an agent guessing a subcommand must not interrupt that session.
135
135
  const args = [...rest];
136
136
  const wantsJson = takeFlag(args, "--json");
137
137
  const yes = takeFlag(args, "--yes");
138
- if (!yes) throw new CliUsageError("system codex-restart requires --yes", USAGE);
138
+ if (!yes) throw new CliUsageError("system codex-restart requires --yes: this fully quits and relaunches the Codex desktop app and restarts its app-servers", USAGE);
139
139
  rejectArgs(args, USAGE);
140
- printData(await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps), wantsJson, ["Codex app-server restart requested."]);
140
+ printData(await runtimeRequest("/api/system/codex-restart", { method: "POST" }, deps), wantsJson, ["Codex desktop app and app-server restart requested."]);
141
141
  } else if (sub === "update") await update(rest, deps);
142
142
  else throw new CliUsageError(`unknown system command ${sub}`, USAGE);
143
143
  });
@@ -812,6 +812,7 @@ export interface OpenclawModelEntry {
812
812
  id: string;
813
813
  name: string;
814
814
  contextWindow?: number;
815
+ input?: string[];
815
816
  }
816
817
 
817
818
  export interface OpenclawProviderBlock {
@@ -839,15 +840,15 @@ export interface KimiProviderBlock {
839
840
  /**
840
841
  * `max_context_size` is mandatory and must be positive, so a model with no
841
842
  * authoritative context window is omitted from the document entirely rather
842
- * than guessed at. `capabilities` is never emitted: our catalog does not
843
- * assert them, and Kimi's own inference works off OpenAI-style name prefixes
844
- * that a routed selector will not match.
843
+ * than guessed at. Catalog image input becomes `image_in`; other capabilities
844
+ * are not inferred from routed model names.
845
845
  */
846
846
  export interface KimiModelBlock {
847
847
  provider: string;
848
848
  model: string;
849
849
  max_context_size: number;
850
850
  display_name?: string;
851
+ capabilities?: ["image_in"];
851
852
  }
852
853
 
853
854
  export interface KimiGeneratedConfig {
@@ -974,10 +975,12 @@ function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig {
974
975
  function buildOpenclawClientConfig(ctx: ExportContext): OpenclawGeneratedConfig {
975
976
  const models: OpenclawModelEntry[] = normalizeExportModels(ctx.models).map(model => {
976
977
  const context = authoritativeContextWindow(model.contextWindow);
978
+ const input = [...new Set(model.inputModalities?.filter(value => ["text", "image", "video", "audio"].includes(value)))];
977
979
  return {
978
980
  id: model.namespaced,
979
981
  name: exportModelLabel(model),
980
982
  ...(context !== undefined ? { contextWindow: context } : {}),
983
+ ...(input.length > 0 ? { input } : {}),
981
984
  };
982
985
  });
983
986
  const headers = proxyAdmissionHeaders(ctx.config, OPENCLAW_API_KEY_ENV_REF);
@@ -1015,6 +1018,7 @@ function buildKimiClientConfig(ctx: ExportContext): KimiGeneratedConfig {
1015
1018
  model: model.namespaced,
1016
1019
  max_context_size: context,
1017
1020
  ...(model.displayName ? { display_name: model.displayName } : {}),
1021
+ ...(model.inputModalities?.includes("image") ? { capabilities: ["image_in"] as ["image_in"] } : {}),
1018
1022
  };
1019
1023
  }
1020
1024
  return {
@@ -1,17 +1,19 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import type { CodexAccount, OcxConfig } from "../types";
3
3
  import type { CodexAuthContext } from "./auth-context";
4
+ import type { ProviderApiKeySelection } from "../types/provider";
4
5
  import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
5
6
 
6
7
  export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
7
8
 
8
9
  /**
9
- * Account log labels come in two families (#2699):
10
+ * Account log labels come in three families:
10
11
  *
11
12
  * - `p<hex6>` (plus the literal `main`) — a Codex pool account.
12
13
  * - `o<hex6>` — a non-Codex OAuth provider account (xai, cursor, and siblings).
14
+ * - `k<hex32>` — a request-owned API-key selection, scoped to provider and reference.
13
15
  *
14
- * Both are sha256-derived digests, never an email and never a raw provider account id. That is
16
+ * Labels never contain an email, raw key/reference, or raw provider account id. That is
15
17
  * a privacy requirement, not a formatting preference: these labels are written to the usage log
16
18
  * and served over the management API.
17
19
  *
@@ -20,7 +22,16 @@ export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
20
22
  * accepted cost of keeping the existing `p` format byte-compatible.
21
23
  */
22
24
  export const OAUTH_ACCOUNT_LOG_LABEL_RE = /^o[a-f0-9]{6}$/;
23
- export const ACCOUNT_LOG_LABEL_RE = /^(?:main|[po][a-f0-9]{6})$/;
25
+ export const KEY_ACCOUNT_LOG_LABEL_RE = /^k[a-f0-9]{32}$/;
26
+ export const ACCOUNT_LOG_LABEL_RE = /^(?:main|[po][a-f0-9]{6}|k[a-f0-9]{32})$/;
27
+
28
+ /** Digest the request-owned configured selection, never serialize its key/reference. */
29
+ export function apiKeyAccountLogLabel(provider: string, selection: ProviderApiKeySelection | undefined): `k${string}` | undefined {
30
+ if (!selection || typeof selection.reference !== "string" || !selection.reference.length) return undefined;
31
+ return `k${createHash("sha256").update(JSON.stringify([
32
+ "ocx-key-account-v1", provider, selection.entryId ?? null, selection.reference,
33
+ ])).digest("hex").slice(0, 32)}`;
34
+ }
24
35
 
25
36
  export function oauthAccountLogLabel(accountId: string, provider = ""): string {
26
37
  return `o${createHash("sha256").update(`${provider}\0${accountId}`).digest("hex").slice(0, 6)}`;
@@ -11,6 +11,8 @@ import { getMainChatgptAccountId, readCodexTokensResult } from "./auth-collision
11
11
  import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
12
12
  import { clearAccountQuota } from "./quota";
13
13
  import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } from "./routing";
14
+
15
+ import { clearCodexPoolRefreshFailure } from "./pool-refresh-backoff";
14
16
  import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
15
17
  import { clearMainAccountCredentialPresence, clearMainAccountInfoCache, observeMainQuotaCredential, observeMainQuotaIdentity } from "./main-account-cache";
16
18
  import { extractAccountIdClaims } from "../oauth/chatgpt";
@@ -41,6 +43,7 @@ export function purgeCodexAccountRuntimeState(accountId: string): void {
41
43
  clearAccountQuota(accountId);
42
44
  clearThreadAccountMapForAccount(accountId);
43
45
  clearCodexUpstreamHealthForAccount(accountId);
46
+ clearCodexPoolRefreshFailure(accountId);
44
47
  if (accountId === MAIN_CODEX_ACCOUNT_ID) {
45
48
  clearMainAccountInfoCache();
46
49
  clearMainAccountCredentialPresence();