@bitkyc08/opencodex 2.88.0 → 2.90.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 (291) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-DgxkE6Md.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
  6. package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
  7. package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +2 -2
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic.ts +10 -3
  13. package/src/adapters/base.ts +4 -0
  14. package/src/adapters/claude-cli/adapter.ts +13 -7
  15. package/src/adapters/claude-cli/usage-admission.ts +14 -2
  16. package/src/adapters/coding-agent/protocol.ts +6 -0
  17. package/src/adapters/coding-agent/turn.ts +14 -4
  18. package/src/adapters/cursor/protobuf-request.ts +3 -0
  19. package/src/adapters/google-http.ts +3 -2
  20. package/src/adapters/google-progress-history.ts +19 -1
  21. package/src/adapters/google-vertex-endpoint.ts +67 -0
  22. package/src/adapters/google.ts +62 -37
  23. package/src/adapters/image.ts +5 -3
  24. package/src/adapters/input-media-guard.ts +5 -2
  25. package/src/adapters/kiro-refusal.ts +41 -1
  26. package/src/adapters/ollama-native.ts +1 -0
  27. package/src/adapters/openai-chat/messages.ts +34 -4
  28. package/src/adapters/openai-chat/passthrough.ts +37 -4
  29. package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
  30. package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
  31. package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
  32. package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
  33. package/src/adapters/openai-chat.ts +7 -6
  34. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  35. package/src/adapters/openai-responses/opengateway-input.ts +139 -0
  36. package/src/adapters/openai-responses/passthrough.ts +22 -13
  37. package/src/adapters/openai-responses/reasoning.ts +68 -1
  38. package/src/adapters/openai-responses/transport.ts +13 -1
  39. package/src/adapters/registry.ts +1 -1
  40. package/src/adapters/upstream-http-error.ts +3 -2
  41. package/src/adapters/zed.ts +7 -4
  42. package/src/bridge/internal.ts +11 -1
  43. package/src/bridge/response-json.ts +2 -2
  44. package/src/bridge/sse.ts +10 -4
  45. package/src/claude/desktop-3p-library.ts +43 -3
  46. package/src/claude/desktop-3p.ts +20 -10
  47. package/src/claude/desktop-picker-profile.ts +18 -9
  48. package/src/claude/desktop-remote-store-io.ts +2 -2
  49. package/src/claude/inbound.ts +12 -26
  50. package/src/claude/outbound.ts +5 -0
  51. package/src/cli/account-api.ts +13 -1
  52. package/src/cli/account-extended.ts +17 -5
  53. package/src/cli/account-key-quota.ts +4 -1
  54. package/src/cli/account.ts +1 -1
  55. package/src/cli/capabilities-agents-routing.ts +45 -8
  56. package/src/cli/capabilities-base.ts +5 -5
  57. package/src/cli/capabilities-provider-models.ts +5 -4
  58. package/src/cli/combo-result.ts +8 -2
  59. package/src/cli/combo.ts +17 -0
  60. package/src/cli/ensure-desktop-cold-start.ts +2 -2
  61. package/src/cli/help.ts +1 -1
  62. package/src/cli/message-command.ts +4 -1
  63. package/src/cli/message-remote-args.ts +69 -0
  64. package/src/cli/message-remote-command.ts +77 -0
  65. package/src/cli/provider-runtime.ts +69 -1
  66. package/src/cli/registry.ts +3 -3
  67. package/src/codex/account-lifecycle.ts +19 -6
  68. package/src/codex/account-pause.ts +6 -1
  69. package/src/codex/account-runtime-state.ts +5 -0
  70. package/src/codex/account-store.ts +55 -9
  71. package/src/codex/account-usability.ts +4 -1
  72. package/src/codex/auth-api/account-list.ts +13 -5
  73. package/src/codex/auth-api/account-pause-group.ts +2 -22
  74. package/src/codex/auth-api/login-collision.ts +59 -0
  75. package/src/codex/auth-api/login-flow.ts +12 -7
  76. package/src/codex/auth-api/main-account-probe.ts +3 -2
  77. package/src/codex/auth-api/pool-mode-gate.ts +81 -13
  78. package/src/codex/auth-collision.ts +31 -4
  79. package/src/codex/auth-context.ts +57 -10
  80. package/src/codex/catalog/aggregation.ts +15 -0
  81. package/src/codex/catalog/auto-review.ts +2 -1
  82. package/src/codex/catalog/build-entries.ts +17 -7
  83. package/src/codex/catalog/derive-entry.ts +3 -1
  84. package/src/codex/catalog/metadata.ts +2 -0
  85. package/src/codex/catalog/model-visibility.ts +0 -2
  86. package/src/codex/catalog/ownership.ts +71 -0
  87. package/src/codex/catalog/parsing.ts +12 -3
  88. package/src/codex/catalog/retained-sync.ts +2 -1
  89. package/src/codex/catalog/routed-removal.ts +4 -4
  90. package/src/codex/cli-update-plan.ts +18 -3
  91. package/src/codex/collision-identity.ts +78 -0
  92. package/src/codex/convergence.ts +2 -1
  93. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  94. package/src/codex/low-quota-protection.ts +57 -31
  95. package/src/codex/main-account-cache.ts +77 -3
  96. package/src/codex/main-account-hard-lock.ts +14 -0
  97. package/src/codex/main-account.ts +6 -2
  98. package/src/codex/native-residue.ts +2 -3
  99. package/src/codex/quota.ts +26 -2
  100. package/src/codex/routing/cooldown-math.ts +5 -0
  101. package/src/codex/routing/failure-window.ts +61 -9
  102. package/src/codex/routing.ts +4 -3
  103. package/src/codex/shim-diagnostics.ts +2 -1
  104. package/src/codex/shim-fingerprint.ts +11 -2
  105. package/src/codex/shim-templates.ts +16 -5
  106. package/src/codex/shim-windows-refresh.ts +224 -0
  107. package/src/codex/shim.ts +45 -21
  108. package/src/combos/failover.ts +10 -2
  109. package/src/combos/jev-model-backend.ts +30 -7
  110. package/src/combos/jev-model-request.ts +20 -0
  111. package/src/combos/jev-quota-activation.ts +153 -0
  112. package/src/combos/jev-quota-config.ts +62 -0
  113. package/src/combos/jev-quota-route.ts +26 -0
  114. package/src/combos/jev-quota.ts +88 -0
  115. package/src/combos/jev.ts +25 -12
  116. package/src/combos/types.ts +27 -0
  117. package/src/compatibility/index.ts +3 -0
  118. package/src/compatibility/openai-responses.ts +8 -1
  119. package/src/compatibility/opengateway-responses.ts +61 -0
  120. package/src/config/account-auto-pause-revisions.ts +136 -0
  121. package/src/config/diagnostics.ts +13 -0
  122. package/src/config/live-reconcile.ts +9 -0
  123. package/src/config/persist-unlocked.ts +10 -1
  124. package/src/config/provider-validation.ts +7 -2
  125. package/src/config/removed-opencode-free.ts +135 -0
  126. package/src/config/schema/account-auto-pause.ts +14 -0
  127. package/src/config/schema/config-schema.ts +15 -0
  128. package/src/config/schema/leaf-validators.ts +7 -0
  129. package/src/config.ts +10 -3
  130. package/src/generated/compatibility-version.json +476 -216
  131. package/src/generated/model-metadata.ts +1 -1
  132. package/src/images/loop.ts +34 -11
  133. package/src/lib/account-auto-pause-response.ts +16 -0
  134. package/src/lib/bun-runtime.ts +18 -1
  135. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  136. package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
  137. package/src/lib/decision-quota-sink.ts +76 -0
  138. package/src/lib/provider-admission-wait.ts +14 -0
  139. package/src/lib/service-secrets.ts +37 -2
  140. package/src/lib/state-store-registrations.ts +25 -0
  141. package/src/lib/translator-budget.ts +1 -1
  142. package/src/lib/upstream-retry.ts +15 -0
  143. package/src/link/ssh-argv.ts +3 -0
  144. package/src/messaging/envelope.ts +6 -3
  145. package/src/messaging/remote-auth.ts +83 -0
  146. package/src/messaging/remote-bridge.ts +270 -0
  147. package/src/messaging/remote-contract.ts +108 -0
  148. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  149. package/src/messaging/remote-enrollment.ts +244 -0
  150. package/src/messaging/remote-envelope.ts +40 -0
  151. package/src/messaging/remote-files.ts +117 -0
  152. package/src/messaging/remote-owner.ts +169 -0
  153. package/src/messaging/remote-ports.ts +56 -0
  154. package/src/messaging/remote-process.ts +131 -0
  155. package/src/messaging/remote-replies.ts +98 -0
  156. package/src/messaging/remote-rpc-admission.ts +32 -0
  157. package/src/messaging/remote-send.ts +72 -0
  158. package/src/messaging/remote-store.ts +111 -0
  159. package/src/messaging/remote-tunnels.ts +65 -0
  160. package/src/messaging/rpc.ts +13 -1
  161. package/src/oauth/account-auto-pause-policy.ts +66 -0
  162. package/src/oauth/account-auto-pause.ts +171 -0
  163. package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
  164. package/src/oauth/account-import/parser.ts +15 -2
  165. package/src/oauth/account-import/types.ts +7 -0
  166. package/src/oauth/account-quota-rank.ts +2 -1
  167. package/src/oauth/anthropic-account-refusal.ts +6 -0
  168. package/src/oauth/anthropic-model-family.ts +8 -0
  169. package/src/oauth/anthropic-model-quota.ts +2 -6
  170. package/src/oauth/api-key-pool-preserve.ts +37 -0
  171. package/src/oauth/chatgpt-shared.ts +10 -1
  172. package/src/oauth/dsh.ts +45 -2
  173. package/src/oauth/index.ts +5 -35
  174. package/src/oauth/kiro.ts +44 -1
  175. package/src/oauth/login-flow-state.ts +11 -2
  176. package/src/oauth/store.ts +35 -8
  177. package/src/oauth/types.ts +2 -0
  178. package/src/plugins/upstream-hooks.ts +14 -1
  179. package/src/providers/api-key-pause.ts +32 -0
  180. package/src/providers/api-key-resolve.ts +10 -4
  181. package/src/providers/api-key-selection.ts +6 -1
  182. package/src/providers/api-keys.ts +8 -4
  183. package/src/providers/claude-cli-usage.ts +25 -5
  184. package/src/providers/initial-model-selection.ts +2 -2
  185. package/src/providers/key-failover.ts +21 -13
  186. package/src/providers/key-store.ts +8 -3
  187. package/src/providers/mistral-transport.ts +207 -0
  188. package/src/providers/model-rename-fields.ts +1 -0
  189. package/src/providers/openai-sidecar.ts +5 -0
  190. package/src/providers/opencode-zen-rate-limit.ts +6 -72
  191. package/src/providers/quota/account-cache.ts +15 -1
  192. package/src/providers/quota/anthropic-account-quota.ts +20 -7
  193. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  194. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  195. package/src/providers/quota/antigravity.ts +45 -8
  196. package/src/providers/quota/failure-codes.ts +18 -0
  197. package/src/providers/quota/report-cache.ts +14 -0
  198. package/src/providers/quota/vendor-probes-oauth.ts +15 -6
  199. package/src/providers/quota-decision-publication.ts +77 -0
  200. package/src/providers/quota-decision-snapshot.ts +145 -0
  201. package/src/providers/quota-routing-cache.ts +4 -1
  202. package/src/providers/quota-types.ts +6 -1
  203. package/src/providers/quota.ts +8 -3
  204. package/src/providers/registry/entries-extended.ts +14 -53
  205. package/src/providers/registry/model-seeds.ts +1 -1
  206. package/src/providers/registry.ts +1 -2
  207. package/src/providers/routed-config.ts +1 -1
  208. package/src/providers/stale-vision-classification-migration.ts +1 -1
  209. package/src/providers/xai-transport.ts +2 -1
  210. package/src/responses/audio.ts +46 -0
  211. package/src/responses/input-media.ts +37 -15
  212. package/src/responses/parser-content.ts +22 -27
  213. package/src/responses/parser.ts +18 -10
  214. package/src/responses/reasoning-retention.ts +218 -11
  215. package/src/responses/schema.ts +3 -4
  216. package/src/responses/web-search-replay.ts +147 -0
  217. package/src/routing/compatibility/behavior.ts +4 -1
  218. package/src/server/auth-cors.ts +7 -0
  219. package/src/server/background-lifecycle.ts +2 -1
  220. package/src/server/chat-native.ts +36 -20
  221. package/src/server/claude-messages.ts +2 -1
  222. package/src/server/index/optional-listeners.ts +6 -0
  223. package/src/server/index/serve-options.ts +8 -2
  224. package/src/server/index/websocket-handler.ts +10 -0
  225. package/src/server/lifecycle.ts +17 -1
  226. package/src/server/management/combo-routes.ts +17 -2
  227. package/src/server/management/oauth-account-routes.ts +12 -2
  228. package/src/server/management/provider-completion-test.ts +289 -0
  229. package/src/server/management/provider-routes.ts +9 -1
  230. package/src/server/management/route-registry.ts +2 -0
  231. package/src/server/messages-native.ts +29 -4
  232. package/src/server/messages-shared.ts +4 -1
  233. package/src/server/request-log.ts +23 -9
  234. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  235. package/src/server/responses/adapter-continuation.ts +2 -2
  236. package/src/server/responses/adapter-dispatch.ts +79 -30
  237. package/src/server/responses/codex-ws-exchange.ts +3 -0
  238. package/src/server/responses/codex-ws-metadata.ts +44 -0
  239. package/src/server/responses/codex-ws-session.ts +21 -1
  240. package/src/server/responses/combo-session-recall.ts +25 -0
  241. package/src/server/responses/combo-stream-preflight.ts +10 -3
  242. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  243. package/src/server/responses/compact.ts +10 -5
  244. package/src/server/responses/context-overflow.ts +141 -2
  245. package/src/server/responses/core-auth.ts +20 -0
  246. package/src/server/responses/core-codex-account.ts +28 -15
  247. package/src/server/responses/core-combo.ts +34 -5
  248. package/src/server/responses/core-opaque-recovery.ts +20 -11
  249. package/src/server/responses/core.ts +12 -9
  250. package/src/server/responses/deepseek-image-files.ts +121 -0
  251. package/src/server/responses/empty-completion-guard.ts +10 -0
  252. package/src/server/responses/fetch-helpers.ts +65 -14
  253. package/src/server/responses/input-admission.ts +6 -2
  254. package/src/server/responses/jev-model-invoke.ts +13 -14
  255. package/src/server/responses/main-hard-lock-routing.ts +33 -8
  256. package/src/server/responses/native-steering-log.ts +2 -2
  257. package/src/server/responses/opengateway-completion-guard.ts +232 -0
  258. package/src/server/responses/passthrough-completion.ts +41 -0
  259. package/src/server/responses/passthrough-delivery.ts +5 -2
  260. package/src/server/responses/passthrough-dispatch.ts +42 -45
  261. package/src/server/responses/passthrough-key-rotation.ts +40 -0
  262. package/src/server/responses/passthrough-observations.ts +38 -0
  263. package/src/server/responses/reasoning-archive-error.ts +21 -0
  264. package/src/server/responses/request-compression.ts +98 -0
  265. package/src/server/responses/request-prepare.ts +11 -1
  266. package/src/server/responses/request-transport.ts +92 -5
  267. package/src/server/responses/run-turn-execution.ts +2 -0
  268. package/src/server/responses/sidecar-execution.ts +16 -3
  269. package/src/server/responses/terminal-guard.ts +2 -0
  270. package/src/server/startup-action-control.ts +2 -2
  271. package/src/server/startup-health-cache.ts +2 -2
  272. package/src/server/windows-tray-control.ts +2 -2
  273. package/src/service/definition.ts +9 -1
  274. package/src/service/orchestration.ts +17 -0
  275. package/src/types/account-auto-pause.ts +15 -0
  276. package/src/types/config.ts +23 -1
  277. package/src/types/provider.ts +11 -5
  278. package/src/types/request.ts +36 -3
  279. package/src/types.ts +2 -0
  280. package/src/usage/cost.ts +17 -1
  281. package/src/usage/expected-prices.ts +3 -1
  282. package/src/usage/jev-stats.ts +4 -0
  283. package/src/usage/log.ts +2 -0
  284. package/src/usage/summary.ts +7 -0
  285. package/src/web-search/loop.ts +39 -13
  286. package/src/web-search/progress-stream.ts +4 -1
  287. package/src/web-search/run-turn-loop.ts +9 -3
  288. package/src/web-search/sidecar-search-replay.ts +96 -0
  289. package/gui/dist/assets/App-CHC89EXQ.js +0 -58
  290. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
  291. package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
@@ -1,3 +1,4 @@
1
+ import { normalizeJevQuotaTiers, type ResolvedJevQuotaTiers } from "./jev-quota-config";
1
2
  import { isCodexReasoningEffort, isDeclaredReasoningEffort } from "../reasoning-effort";
2
3
  import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
3
4
  import type { OcxComboConfig, OcxComboCooldownWaitPolicy, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
@@ -68,12 +69,16 @@ export interface NormalizedComboConfig {
68
69
  nativeAlias: boolean;
69
70
  /** Display-only label for the catalog row, or null when unset. */
70
71
  displayName: string | null;
72
+ /** Display-only catalog row description; absent keeps the generated text (#7229). */
73
+ description?: string;
71
74
  /** JEV decision service provider id; absent means the canonical `jev` service. */
72
75
  decisionProvider?: string;
73
76
  /** Ordinary inference route used for JEV decisions. */
74
77
  decisionModel?: string;
75
78
  /** JEV decision deadline override; absent keeps the default four-second deadline. */
76
79
  decisionTimeoutMs?: number;
80
+ decisionQuotaSignals?: boolean;
81
+ decisionQuotaTiers?: ResolvedJevQuotaTiers;
77
82
  targets: NormalizedComboTarget[];
78
83
  }
79
84
 
@@ -137,6 +142,7 @@ export interface ComboValidationOptions {
137
142
  excludeComboId?: string;
138
143
  }
139
144
 
145
+ /** Validate a raw combo definition against the configured providers and return every issue found, without mutating the input. */
140
146
  export function comboConfigIssues(
141
147
  id: string,
142
148
  raw: unknown,
@@ -277,6 +283,16 @@ export function comboConfigIssues(
277
283
  });
278
284
  }
279
285
  }
286
+ if (body.description !== undefined) {
287
+ if (typeof body.description !== "string") {
288
+ issues.push({ path: ["description"], message: "description must be a string" });
289
+ } else if (body.description.trim().length > 128 || /\p{Cc}/u.test(body.description)) {
290
+ issues.push({
291
+ path: ["description"],
292
+ message: "description must be at most 128 characters and contain no control characters",
293
+ });
294
+ }
295
+ }
280
296
  const alias = typeof body.alias === "string" ? body.alias.trim() : "";
281
297
  const nativeAlias = body.nativeAlias === true;
282
298
  if (nativeAlias && !SUPPORTED_NATIVE_OPENAI_SLUGS.has(alias)) {
@@ -288,6 +304,12 @@ export function comboConfigIssues(
288
304
  if (nativeAlias && (typeof body.displayName !== "string" || body.displayName.trim().length === 0)) {
289
305
  issues.push({ path: ["displayName"], message: "displayName is required for native aliases" });
290
306
  }
307
+ for (const field of ["decisionQuotaSignals", "decisionQuotaTiers"] as const) {
308
+ if (body[field] === undefined || body[field] === null) continue;
309
+ if (body.strategy !== "jev") issues.push({ path: [field], message: `${field} is only valid with strategy "jev"` });
310
+ if (field === "decisionQuotaSignals" ? typeof body[field] !== "boolean" : !normalizeJevQuotaTiers(body[field]))
311
+ issues.push({ path: [field], message: field === "decisionQuotaSignals" ? "decisionQuotaSignals must be a boolean" : "decisionQuotaTiers must contain finite 0..100 strictly ascending thresholds" });
312
+ }
291
313
  if (body.decisionProvider !== undefined && body.decisionProvider !== null) {
292
314
  const decisionProvider = typeof body.decisionProvider === "string" ? body.decisionProvider.trim() : "";
293
315
  if (!decisionProvider) {
@@ -480,9 +502,11 @@ export function comboConfigError(
480
502
  return comboConfigIssues(id, raw, providers, options)[0]?.message ?? null;
481
503
  }
482
504
 
505
+ /** Normalize a validated combo definition into its canonical form: trimmed strings, defaults and the opt-in quota signal fields. */
483
506
  export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig {
484
507
  const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
485
508
  const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : "";
509
+ const description = typeof raw.description === "string" ? raw.description.trim() : "";
486
510
  const decisionProvider = typeof raw.decisionProvider === "string" ? raw.decisionProvider.trim() : "";
487
511
  const decisionModel = typeof raw.decisionModel === "string" ? raw.decisionModel.trim() : "";
488
512
  const defaultEffort = typeof raw.defaultEffort === "string" && isCodexReasoningEffort(raw.defaultEffort)
@@ -502,10 +526,13 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
502
526
  alias: alias || null,
503
527
  nativeAlias: raw.nativeAlias === true,
504
528
  displayName: displayName || null,
529
+ ...(description ? { description } : {}),
505
530
  // Explicit "jev" is the default and stays sparse.
506
531
  ...(decisionProvider && decisionProvider !== CANONICAL_JEV_DECISION_PROVIDER ? { decisionProvider } : {}),
507
532
  ...(decisionModel ? { decisionModel } : {}),
508
533
  ...(typeof raw.decisionTimeoutMs === "number" ? { decisionTimeoutMs: raw.decisionTimeoutMs } : {}),
534
+ ...(raw.strategy === "jev" && raw.decisionQuotaSignals === true ? { decisionQuotaSignals: true } : {}),
535
+ ...(raw.strategy === "jev" && normalizeJevQuotaTiers(raw.decisionQuotaTiers) ? { decisionQuotaTiers: normalizeJevQuotaTiers(raw.decisionQuotaTiers)! } : {}),
509
536
  targets: raw.targets.map(target => ({
510
537
  provider: target.provider.trim(),
511
538
  model: target.model.trim(),
@@ -1,4 +1,5 @@
1
1
  import { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
2
+ import { OPENGATEWAY_KEY_GLM53_RESPONSES_MANIFEST } from "./opengateway-responses";
2
3
  import type { CompatibilityManifestV1 } from "./manifest";
3
4
 
4
5
  export {
@@ -16,9 +17,11 @@ export type {
16
17
  CompatibilitySubjectV1,
17
18
  } from "./manifest";
18
19
  export { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
20
+ export { OPENGATEWAY_KEY_GLM53_RESPONSES_MANIFEST } from "./opengateway-responses";
19
21
 
20
22
  export const COMPATIBILITY_MANIFESTS: readonly CompatibilityManifestV1[] = Object.freeze([
21
23
  OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST,
24
+ OPENGATEWAY_KEY_GLM53_RESPONSES_MANIFEST,
22
25
  ]);
23
26
 
24
27
  export function getCompatibilityManifest(id: string): CompatibilityManifestV1 | undefined {
@@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
9
9
  export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
10
10
  schemaVersion: 1,
11
11
  id: "openai.codex-forward.gpt-5-6-sol.responses",
12
- version: "1.3.0",
12
+ version: "1.4.0",
13
13
  subject: {
14
14
  providerId: "openai",
15
15
  baseUrl: "https://chatgpt.com/backend-api/codex",
@@ -117,5 +117,12 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife
117
117
  limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
118
118
  evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-options-removed"] }],
119
119
  },
120
+ {
121
+ id: "custom-tool-grammar",
122
+ feature: "request.custom_tools.grammar",
123
+ disposition: "passthrough",
124
+ summary: "A custom tool's grammar format, including its definition, reaches the backend unchanged.",
125
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-grammar-preserved"] }],
126
+ },
120
127
  ],
121
128
  } as const);
@@ -0,0 +1,61 @@
1
+ import { defineCompatibilityManifest } from "./manifest";
2
+
3
+ const FIXTURE_ID = "opengateway-key-glm53-responses-v1";
4
+
5
+ /**
6
+ * A routed Responses subject whose destination rejects native custom tools, so OpenCodex lowers
7
+ * them to functions before dispatch. The claims describe that local wire transformation only;
8
+ * they do not certify how the upstream model treats the lowered declaration.
9
+ */
10
+ export const OPENGATEWAY_KEY_GLM53_RESPONSES_MANIFEST = defineCompatibilityManifest({
11
+ schemaVersion: 1,
12
+ id: "opengateway.key.glm-5-3-flash-ultrafast.responses",
13
+ version: "1.0.0",
14
+ subject: {
15
+ providerId: "opengateway",
16
+ baseUrl: "https://apis.opengateway.ai/v1",
17
+ adapterId: "openai-responses",
18
+ authMode: "key",
19
+ inboundProtocol: "responses",
20
+ upstreamProtocol: "openai-responses",
21
+ modelIds: ["z-ai/glm-5.3-flash-ultrafast"],
22
+ },
23
+ claims: [
24
+ {
25
+ id: "custom-tools",
26
+ feature: "request.custom_tools",
27
+ disposition: "translated",
28
+ summary: "Custom tool declarations are sent as functions that take one raw string input.",
29
+ limitation: "The destination rejects native custom tools, so each declaration is lowered to a function with a single required string parameter named input.",
30
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-lowered-to-function"] }],
31
+ },
32
+ {
33
+ id: "custom-tool-grammar",
34
+ feature: "request.custom_tools.grammar",
35
+ disposition: "degraded",
36
+ summary: "The tool and its description survive, but its grammar does not reach the destination.",
37
+ limitation: "The format, including any grammar definition, is removed during lowering, so the upstream model is not constrained by it; only the string input type remains.",
38
+ evidence: [{
39
+ kind: "fixture",
40
+ id: FIXTURE_ID,
41
+ assertionIds: ["custom-tool-grammar-removed", "custom-tool-lowered-to-function"],
42
+ }],
43
+ },
44
+ {
45
+ id: "custom-tool-call-history",
46
+ feature: "continuation.custom_tool_call",
47
+ disposition: "translated",
48
+ summary: "Replayed custom tool calls are sent as function calls with the same call_id and name.",
49
+ limitation: "The raw input is wrapped as the JSON arguments object {\"input\": <raw input>}.",
50
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-call-lowered"] }],
51
+ },
52
+ {
53
+ id: "custom-tool-output-history",
54
+ feature: "continuation.custom_tool_call_output",
55
+ disposition: "translated",
56
+ summary: "Replayed custom tool outputs are sent as function call outputs with the same call_id and output.",
57
+ limitation: "Only the item type changes from custom_tool_call_output to function_call_output.",
58
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["custom-tool-output-lowered"] }],
59
+ },
60
+ ],
61
+ } as const);
@@ -0,0 +1,136 @@
1
+ import * as z from "zod/v4";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
4
+ import type { OcxConfig } from "../types";
5
+ import { getConfigPath } from "./paths";
6
+
7
+ type PolicyConfig = { providers?: unknown; accountAutoPausePolicyRevisions?: unknown };
8
+ // fingerprint follows the live config; diskFingerprint follows the last observed disk state, so a
9
+ // detached load compares disk with disk and still fences an observed external policy change.
10
+ type Observed = { diskRevision: string; localGen: number; unknown: boolean; fingerprint: string; diskFingerprint: string };
11
+ export type AccountAutoPauseFence = { configPath: string; diskRevision: string; localGen: number; threshold: number; route: string };
12
+ const observed = new Map<string, Map<string, Observed>>();
13
+ // One process-wide allocator for every new entry and every advance: a deleted and recreated name
14
+ // can never reproduce an old {diskRevision, localGen} pair, even when both read "legacy".
15
+ let nextGeneration = 0;
16
+ const allocateGeneration = (): number => ++nextGeneration;
17
+ function hasProvider(config: PolicyConfig | undefined, name: string): boolean {
18
+ const providers = record(config?.providers);
19
+ return !!providers && Object.hasOwn(providers, name);
20
+ }
21
+ const UUID = z.uuid();
22
+ function record(value: unknown): Record<string, unknown> | undefined {
23
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
24
+ }
25
+ function revisions(config: PolicyConfig): Record<string, string> | undefined | null {
26
+ const value = config.accountAutoPausePolicyRevisions;
27
+ if (value === undefined) return undefined;
28
+ const map = record(value);
29
+ return map && Object.values(map).every(v => typeof v === "string" && UUID.safeParse(v).success) ? map as Record<string, string> : null;
30
+ }
31
+ function revisionFor(map: Record<string, string> | undefined | null, name: string): string | undefined {
32
+ return map && Object.hasOwn(map, name) ? map[name] : undefined;
33
+ }
34
+ function provider(config: PolicyConfig, name: string): Record<string, unknown> | undefined {
35
+ const providers = record(config.providers);
36
+ return providers && Object.hasOwn(providers, name) ? record(providers[name]) : undefined;
37
+ }
38
+ function route(row: Record<string, unknown> | undefined): string {
39
+ return JSON.stringify([row?.baseUrl, row?.adapter, row?.authMode]);
40
+ }
41
+ export function accountAutoPauseFingerprint(row: unknown): string {
42
+ const p = record(row), policy = record(p?.accountAutoPause);
43
+ return JSON.stringify([!!p, policy?.enabled === true, policy?.consecutiveFailures ?? 2, p?.disabled === true, route(p)]);
44
+ }
45
+ export function snapshotAutoPauseFingerprints(config: PolicyConfig): Map<string, string> {
46
+ return new Map(Object.entries(record(config.providers) ?? {}).map(([name, row]) => [name, accountAutoPauseFingerprint(row)]));
47
+ }
48
+ /** Caller revisions never authorize a write. The literal pre-write file owns the epoch. */
49
+ export function normalizeAccountAutoPauseRevisions(before: PolicyConfig | undefined, persisted: OcxConfig): void {
50
+ const old = revisions(before ?? {});
51
+ const names = new Set(Object.keys(old ?? {}));
52
+ for (const config of [before, persisted]) {
53
+ for (const [name, row] of Object.entries(record(config?.providers) ?? {})) {
54
+ if (record(row)?.accountAutoPause !== undefined) names.add(name);
55
+ }
56
+ }
57
+ // Records follow the providers that exist after disk-only preservation; a deleted provider's
58
+ // record is reclaimed and a recreated one is minted a fresh UUID.
59
+ const entries = [...names].filter(name => hasProvider(persisted, name)).map(name => [name, revisionFor(old, name)
60
+ && accountAutoPauseFingerprint(provider(before ?? {}, name)) === accountAutoPauseFingerprint(provider(persisted, name))
61
+ ? revisionFor(old, name)! : randomUUID()]);
62
+ if (entries.length) persisted.accountAutoPausePolicyRevisions = Object.fromEntries(entries);
63
+ else delete persisted.accountAutoPausePolicyRevisions;
64
+ }
65
+ /** Adopt disk epochs even when an external ABA left the effective policy unchanged. */
66
+ export function observeAccountAutoPauseRevisions(config: PolicyConfig, disk: PolicyConfig = config, previous?: Map<string, string>): void {
67
+ const path = getConfigPath();
68
+ let entries = observed.get(path);
69
+ if (!entries) { entries = new Map(); observed.set(path, entries); }
70
+ const map = revisions(disk);
71
+ const names = new Set([...entries.keys(), ...Object.keys(record(config.providers) ?? {}), ...Object.keys(map ?? {})]);
72
+ for (const name of names) {
73
+ if (!hasProvider(config, name) && !hasProvider(disk, name)) { entries.delete(name); continue; }
74
+ const fingerprint = accountAutoPauseFingerprint(provider(config, name));
75
+ const old = entries.get(name);
76
+ const prior = previous?.get(name) ?? old?.fingerprint ?? fingerprint;
77
+ entries.set(name, { fingerprint, diskRevision: revisionFor(map, name) ?? "legacy", unknown: map === null,
78
+ diskFingerprint: accountAutoPauseFingerprint(provider(disk, name)),
79
+ localGen: !old || prior !== fingerprint ? allocateGeneration() : old.localGen });
80
+ }
81
+ }
82
+ /**
83
+ * A loaded snapshot adopts the disk epoch and compares its policy with the previously observed
84
+ * disk policy. Many callers read detached snapshots; comparing one with the live fingerprint would
85
+ * advance localGen on every live/snapshot difference. A disk-to-disk change still advances it.
86
+ */
87
+ function adoptDiskAutoPauseRevisions(config: PolicyConfig): void {
88
+ const path = getConfigPath();
89
+ let entries = observed.get(path);
90
+ if (!entries) { entries = new Map(); observed.set(path, entries); }
91
+ const map = revisions(config);
92
+ for (const name of new Set([...entries.keys(), ...Object.keys(record(config.providers) ?? {}), ...Object.keys(map ?? {})])) {
93
+ if (!hasProvider(config, name)) { entries.delete(name); continue; }
94
+ const old = entries.get(name);
95
+ const diskRevision = revisionFor(map, name) ?? "legacy";
96
+ const diskFingerprint = accountAutoPauseFingerprint(provider(config, name));
97
+ entries.set(name, old
98
+ ? { ...old, diskRevision, unknown: map === null, diskFingerprint, localGen: old.diskFingerprint !== diskFingerprint ? allocateGeneration() : old.localGen }
99
+ : { fingerprint: diskFingerprint, diskFingerprint, diskRevision, unknown: map === null, localGen: allocateGeneration() });
100
+ }
101
+ }
102
+ /** Test inspection only: observed records for the current config path. */
103
+ export function accountAutoPauseObservedSizeForTests(): number {
104
+ return observed.get(getConfigPath())?.size ?? 0;
105
+ }
106
+ /** Initial load is an observation, never a persistence operation. */
107
+ export function observeLoadedAutoPauseConfig(config: OcxConfig): OcxConfig {
108
+ adoptDiskAutoPauseRevisions(config);
109
+ return config;
110
+ }
111
+ export function captureAccountAutoPauseFence(config: OcxConfig, name: string): AccountAutoPauseFence | undefined {
112
+ // Only a config path no load or save has observed may be seeded from the caller's object. Once a
113
+ // path is observed, a missing name means an authoritative observation removed it, and a stale
114
+ // live snapshot must not re-enroll it; adoption by load, reload or save is required.
115
+ if (!observed.has(getConfigPath())) observeAccountAutoPauseRevisions(config);
116
+ const state = observed.get(getConfigPath())?.get(name);
117
+ const p = provider(config, name), policy = record(p?.accountAutoPause);
118
+ const threshold = policy?.consecutiveFailures ?? 2;
119
+ if (!state || state.unknown || !p || p.disabled === true || policy?.enabled !== true || typeof threshold !== "number" || !Number.isInteger(threshold) || threshold < 1 || threshold > 100) return;
120
+ return { configPath: getConfigPath(), diskRevision: state.diskRevision, localGen: state.localGen, threshold, route: route(p) };
121
+ }
122
+ export function accountAutoPauseFenceIsLocal(name: string, fence: AccountAutoPauseFence): boolean {
123
+ const state = observed.get(fence.configPath)?.get(name);
124
+ return getConfigPath() === fence.configPath && !!state && !state.unknown
125
+ && state.diskRevision === fence.diskRevision && state.localGen === fence.localGen;
126
+ }
127
+ /** Must run inside the config mutation lock at the final persistence boundary. */
128
+ export function accountAutoPauseFenceMatchesDisk(name: string, fence: AccountAutoPauseFence, fresh?: PolicyConfig): boolean {
129
+ if (!accountAutoPauseFenceIsLocal(name, fence)) return false;
130
+ try {
131
+ const disk = fresh ?? JSON.parse(readFileSync(fence.configPath, "utf8").replace(/^\uFEFF/, "")) as PolicyConfig;
132
+ const map = revisions(disk), p = provider(disk, name), policy = record(p?.accountAutoPause);
133
+ return map !== null && (revisionFor(map, name) ?? "legacy") === fence.diskRevision && !!p && p.disabled !== true
134
+ && policy?.enabled === true && (policy.consecutiveFailures ?? 2) === fence.threshold && route(p) === fence.route;
135
+ } catch { return false; }
136
+ }
@@ -21,6 +21,7 @@ import { isMissingPathError } from "./atomic-write";
21
21
  import { getConfigDir, getConfigPath } from "./paths";
22
22
  import { getDefaultConfig } from "./proxy-env";
23
23
  import { salvageConfigCandidate } from "./salvage";
24
+ import { sanitizeRemovedOpenCodeFreeForLoad } from "./removed-opencode-free";
24
25
  import { sanitizeOpenCodeOverridesForLoad } from "./opencode-wire-upgrade";
25
26
  import {
26
27
  sanitizeReasoningPinsForLoad,
@@ -267,6 +268,16 @@ function upstreamHostCircuitThresholdError(value: unknown): string | null {
267
268
  return `schema_invalid: upstreamHostCircuitThreshold: must be an integer from 0 to ${UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD}`;
268
269
  }
269
270
 
271
+ // The schema degrades an unknown codec to identity so a hand edit cannot break startup; a live write
272
+ // or import must instead be refused, or it would report success and then drop the value (#7247).
273
+ function upstreamRequestCompressionError(value: unknown): string | null {
274
+ const raw = rawConfigRecord(value);
275
+ if (!raw || !Object.hasOwn(raw, "upstreamRequestCompression")) return null;
276
+ const mode = raw.upstreamRequestCompression;
277
+ if (mode === undefined || mode === "off" || mode === "gzip" || mode === "zstd") return null;
278
+ return "schema_invalid: upstreamRequestCompression: must be \"off\", \"gzip\" or \"zstd\"";
279
+ }
280
+
270
281
  function plaintextV2AgentMessagesError(value: unknown): string | null {
271
282
  return malformedPlaintextV2AgentMessagesWarning(value)
272
283
  ? "schema_invalid: plaintextV2AgentMessages: must be a boolean or omitted"
@@ -726,6 +737,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
726
737
  ?? claudeSubagentEffortError(value)
727
738
  ?? appOwnedMemoryBudgetError(value)
728
739
  ?? upstreamHostCircuitThresholdError(value)
740
+ ?? upstreamRequestCompressionError(value)
729
741
  ?? plaintextV2AgentMessagesError(value)
730
742
  ?? agentTaskRecoveryError(value)
731
743
  ?? quotaResetNotifyError(value)
@@ -766,6 +778,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
766
778
  export function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
767
779
  try {
768
780
  const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
781
+ sanitizeRemovedOpenCodeFreeForLoad(parsed);
769
782
  sanitizeOpenCodeOverridesForLoad(parsed);
770
783
  sanitizeReasoningPinsForLoad(parsed);
771
784
  // Same degradation as loadConfig: a hand-edited invalid retryOn429 must not trip the
@@ -1,3 +1,4 @@
1
+ import { observeAccountAutoPauseRevisions, snapshotAutoPauseFingerprints } from "./account-auto-pause-revisions";
1
2
  import type { OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types";
2
3
  import { configReasoningPinsConfigError } from "./provider-validation";
3
4
  import { adoptCustomModelCatalogMigration, projectCustomModelCatalogMigration } from "../codex/custom-model-catalog-migration";
@@ -18,6 +19,7 @@ import { withConfigMutationLockSync, bumpGenerationForCooperatingConfigWrite } f
18
19
  import { ConfigWritePublishedError, persistConfigUnlocked, readRawConfigJson } from "./persist-unlocked";
19
20
  import { configDiagnosticsFromRaw, readConfigDiagnostics, validateConfigCandidate } from "./diagnostics";
20
21
  import { normalizePersistedClaudeCode } from "./load-degrade";
22
+ import { sanitizeRemovedOpenCodeFreeForLoad } from "./removed-opencode-free";
21
23
  import { reconcileApiKeys } from "./api-key-reconcile";
22
24
  import { captureLiveTopologyBaseline, LIVE_TOPOLOGY_KEYS, projectLiveTopology } from "./live-topology";
23
25
 
@@ -161,7 +163,9 @@ export function adoptPersistedProviderIntoLiveConfig(
161
163
  provider: OcxProviderConfig,
162
164
  persistedConfig?: OcxConfig,
163
165
  ): void {
166
+ const pauseBefore = snapshotAutoPauseFingerprints(config);
164
167
  config.providers[name] = structuredClone(provider);
168
+ observeAccountAutoPauseRevisions(config, persistedConfig ?? readRawConfigJson() ?? { accountAutoPausePolicyRevisions: null }, pauseBefore);
165
169
  const baseline = liveConfigBaseline.get(config);
166
170
  if (baseline) baseline.providers[name] = structuredClone(provider);
167
171
  if (persistedConfig) refreshPreservedProviderOwner(config, persistedConfig);
@@ -436,6 +440,7 @@ export function reconcileLiveConfigFromDisk(config: OcxConfig, persistedBaseline
436
440
  if (diagnostics.source === "fallback") {
437
441
  throw new Error(`OAuth config reconciliation failed: ${diagnostics.error ?? "invalid config file"}`);
438
442
  }
443
+ const pauseBefore = snapshotAutoPauseFingerprints(config);
439
444
  const persisted = diagnostics.config;
440
445
  const claudeGuardArmed = claudeCodeBaseline.has(config);
441
446
  const pendingLiveClaudeMutation = claudeGuardArmed
@@ -460,6 +465,7 @@ export function reconcileLiveConfigFromDisk(config: OcxConfig, persistedBaseline
460
465
  else config.claudeCode = structuredClone(persisted.claudeCode);
461
466
  claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
462
467
  }
468
+ observeAccountAutoPauseRevisions(config, persisted, pauseBefore);
463
469
  // The reconciliation may have adopted a providers.<name>.modelCosts edit made
464
470
  // by a cooperating process while the OAuth login was pending; keep the overlay
465
471
  // registry (and the usage-cache overlay version) in sync with the live config.
@@ -526,6 +532,8 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
526
532
  // One authoritative pre-write read feeds both the live-config reconciliation and
527
533
  // custom-model deletion migration. A second read could observe different bytes.
528
534
  const onDisk = readRawConfigJson();
535
+ sanitizeRemovedOpenCodeFreeForLoad(onDisk);
536
+ const pauseBefore = snapshotAutoPauseFingerprints(config);
529
537
  const baseline = liveConfigBaseline.get(config);
530
538
  let diskDeletedKeys = new Set<string>();
531
539
  let persistedSnapshot: OcxConfig | undefined;
@@ -576,6 +584,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
576
584
  }
577
585
  }
578
586
  }
587
+ observeAccountAutoPauseRevisions(config, onDisk ?? config, pauseBefore);
579
588
  applyConfigObjectChildDeletions(config, childDeletions);
580
589
  // Recursive three-way merging preserves sibling rows; explicit PUT/reset
581
590
  // replaces only its model after merging, including absent policy fields.
@@ -1,3 +1,4 @@
1
+ import { normalizeAccountAutoPauseRevisions, observeAccountAutoPauseRevisions, snapshotAutoPauseFingerprints } from "./account-auto-pause-revisions";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { configReasoningPinsConfigError } from "./provider-validation";
3
4
  import type { OcxConfig } from "../types";
@@ -78,6 +79,8 @@ export function persistConfigUnlocked(config: OcxConfig): boolean {
78
79
  const persisted = withPreservedDiskOnlyProviders(config);
79
80
  if (provenanceProjection.configRebaseProvenance === undefined) delete persisted.configRebaseProvenance;
80
81
  else persisted.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
82
+ normalizeAccountAutoPauseRevisions(rawBeforeWrite, persisted);
83
+ const beforeFingerprints = snapshotAutoPauseFingerprints(rawBeforeWrite ?? {});
81
84
  const bytes = JSON.stringify(persisted, null, 2) + "\n";
82
85
  let unchanged = false;
83
86
  try {
@@ -90,6 +93,7 @@ export function persistConfigUnlocked(config: OcxConfig): boolean {
90
93
  // the same bytes (e.g. before a proxy notification), and Logs/Usage must
91
94
  // adopt the overlay without waiting for a changed save or restart.
92
95
  let published = unchanged;
96
+ let observedPublication = false;
93
97
  try {
94
98
  if (!unchanged) {
95
99
  atomicWriteFile(configPath, bytes, undefined, { afterRename: () => { published = true; } });
@@ -97,10 +101,15 @@ export function persistConfigUnlocked(config: OcxConfig): boolean {
97
101
  }
98
102
  // Publication, not successful cache refresh, is the rollback boundary. A
99
103
  // byte-identical save already has the requested state on disk as well.
104
+ observeAccountAutoPauseRevisions(persisted, persisted, beforeFingerprints);
105
+ observedPublication = true;
100
106
  refreshConfigDerivedRegistries(persisted);
101
107
  return !unchanged;
102
108
  } catch (error) {
103
- if (published) throw new ConfigWritePublishedError(error);
109
+ if (published) {
110
+ if (!observedPublication) observeAccountAutoPauseRevisions(persisted, persisted, beforeFingerprints);
111
+ throw new ConfigWritePublishedError(error);
112
+ }
104
113
  throw error;
105
114
  }
106
115
  }
@@ -463,7 +463,7 @@ export function modelCapabilitiesConfigError(value: unknown, allowTombstones = f
463
463
  if (allowTombstones && row === null) continue;
464
464
  if (!capabilityRecord(row)) return "modelCapabilities rows must be plain objects";
465
465
  for (const [axis, declaration] of Object.entries(row)) {
466
- if (!["inputModalities", "contextTier", "video"].includes(axis)) return "modelCapabilities contains an unknown axis";
466
+ if (!["inputModalities", "contextTier", "video", "deepseekImageFiles"].includes(axis)) return "modelCapabilities contains an unknown axis";
467
467
  if (allowTombstones && declaration === null) continue;
468
468
  if (axis === "inputModalities") {
469
469
  if (!Array.isArray(declaration) || declaration.length === 0
@@ -472,6 +472,8 @@ export function modelCapabilitiesConfigError(value: unknown, allowTombstones = f
472
472
  }
473
473
  } else if (axis === "contextTier") {
474
474
  if (declaration !== "default" && declaration !== "long_context") return "modelCapabilities contextTier must be default or long_context";
475
+ } else if (axis === "deepseekImageFiles") {
476
+ if (typeof declaration !== "boolean") return "modelCapabilities deepseekImageFiles must be a boolean";
475
477
  } else {
476
478
  if (!capabilityRecord(declaration) || Object.keys(declaration).some(key => key !== "processing")) {
477
479
  return "modelCapabilities video must be a plain object containing only processing";
@@ -501,6 +503,9 @@ export function mergeModelCapabilities(
501
503
  } else if (axis === "contextTier") {
502
504
  if (value === null) delete row.contextTier;
503
505
  else row.contextTier = value as ModelCapabilities["contextTier"];
506
+ } else if (axis === "deepseekImageFiles") {
507
+ if (value === null) delete row.deepseekImageFiles;
508
+ else row.deepseekImageFiles = value as boolean;
504
509
  } else if (axis === "video") {
505
510
  if (value === null) delete row.video;
506
511
  else {
@@ -528,7 +533,7 @@ export function sanitizeModelCapabilitiesForLoad(value: unknown): Record<string,
528
533
  for (const [id, raw] of Object.entries(value).slice(0, MODEL_DISCOVERY_MAX_MODELS)) {
529
534
  if (!capabilityRecord(raw) || !isValidModelDiscoveryModelId(id) || ["__proto__", "prototype", "constructor"].includes(id)) continue;
530
535
  const row: Record<string, unknown> = {};
531
- for (const axis of ["inputModalities", "contextTier", "video"] as const) {
536
+ for (const axis of ["inputModalities", "contextTier", "video", "deepseekImageFiles"] as const) {
532
537
  if (!Object.hasOwn(raw, axis)) continue;
533
538
  const declaration = raw[axis];
534
539
  if (modelCapabilitiesConfigError({ [id]: { [axis]: declaration } }) === null) row[axis] = declaration;
@@ -0,0 +1,135 @@
1
+ import { getDefaultConfig } from "./proxy-env";
2
+ import { DESKTOP_FAMILIES } from "../claude/desktop-profile";
3
+
4
+ const REMOVED_PROVIDER = "opencode-free";
5
+ let warned = false;
6
+
7
+ function record(value: unknown): Record<string, unknown> | undefined {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value)
9
+ ? value as Record<string, unknown> : undefined;
10
+ }
11
+
12
+ /** Load-only projection, like the OpenCode wire upgrades: never rewrites config.json. */
13
+ export function sanitizeRemovedOpenCodeFreeForLoad(parsed: unknown): void {
14
+ const config = record(parsed);
15
+ if (!config) return;
16
+ const providers = record(config.providers);
17
+ const retiredProviders = new Set([REMOVED_PROVIDER]);
18
+ const retiredRoutes = new Set<string>();
19
+ let changed = false;
20
+ if (providers) {
21
+ for (const [name, value] of Object.entries(providers)) {
22
+ const provider = record(value);
23
+ if (name !== REMOVED_PROVIDER && ![provider?.adapter, provider?.type, provider?.id, provider?.preset, record(provider?.preset)?.id, provider?.presetId, provider?.providerType].includes(REMOVED_PROVIDER)) continue;
24
+ retiredProviders.add(name);
25
+ if (typeof provider?.alias === "string") retiredProviders.add(provider.alias.trim());
26
+ delete providers[name];
27
+ changed = true;
28
+ }
29
+ }
30
+ const retired = (value: unknown): boolean => typeof value === "string"
31
+ && (retiredProviders.has(value.trim()) || retiredProviders.has(value.trim().split("/")[0]!) || retiredRoutes.has(value.trim()));
32
+
33
+ // Prune references in arrays/maps as well as structured provider/model targets. A decision
34
+ // model is required operator intent: disable that combo instead of choosing another service.
35
+ function prune(value: unknown, preserveContainer = false): unknown {
36
+ if (retired(value)) { changed = true; return undefined; }
37
+ if (Array.isArray(value)) return value.map(item => prune(item)).filter(item => item !== undefined);
38
+ const row = record(value);
39
+ if (!row) return value;
40
+ if (!preserveContainer && [row.provider, row.model, row.decisionModel, row.decisionProvider].some(retired)) {
41
+ changed = true;
42
+ return undefined;
43
+ }
44
+ const hadTargets = Array.isArray(row.targets) && row.targets.length > 0;
45
+ const hadCandidates = Array.isArray(row.candidates) && row.candidates.length > 0;
46
+ const hadSourceModels = Array.isArray(row.sourceModels) && row.sourceModels.length > 0;
47
+ for (const [key, child] of Object.entries(row)) {
48
+ // Descriptions and credential/transport data are not model references.
49
+ if (["note", "description", "displayName", "label", "modelProfile", "headers", "apiKey", "apiKeys", "baseUrl", "modelDisplayNames"].includes(key)) continue;
50
+ const next = retired(key) ? undefined : prune(child);
51
+ if (next === undefined) { delete row[key]; changed = true; }
52
+ else row[key] = next;
53
+ }
54
+ // Empty combos/profiles cannot pass schema validation or execute a request.
55
+ if ((hadTargets && Array.isArray(row.targets) && row.targets.length === 0)
56
+ || (hadCandidates && Array.isArray(row.candidates) && row.candidates.length === 0)
57
+ || (hadSourceModels && Array.isArray(row.sourceModels) && row.sourceModels.length === 0)) return undefined;
58
+ return row;
59
+ }
60
+
61
+ // Register removed public aliases before pruning remaining references, including a
62
+ // combo whose decision model points to another combo disabled later in the map.
63
+ let removedRoute: boolean;
64
+ do {
65
+ removedRoute = false;
66
+ for (const [section, namespace] of [["combos", "combo"], ["routingProfiles", "policy"]] as const) {
67
+ const routes = record(config[section]);
68
+ if (!routes) continue;
69
+ for (const [id, value] of Object.entries(routes)) {
70
+ const alias = record(value)?.alias;
71
+ if (prune(value) !== undefined) continue;
72
+ delete routes[id];
73
+ retiredRoutes.add(`${namespace}/${id}`);
74
+ if (typeof alias === "string") retiredRoutes.add(alias.trim());
75
+ changed = removedRoute = true;
76
+ }
77
+ }
78
+ } while (removedRoute);
79
+ // A retired default falls back to the standard built-in OAuth route, without moving a
80
+ // user's choice to a different billable provider. Preserve any configured OpenAI row.
81
+ if (retired(config.defaultProvider)) {
82
+ const defaults = getDefaultConfig();
83
+ config.providers = { ...defaults.providers, ...providers };
84
+ config.defaultProvider = defaults.defaultProvider;
85
+ changed = true;
86
+ }
87
+ const depletedFallbacks = new Set(Object.entries(record(config.subagentModelFallbackByModel) ?? {})
88
+ .filter(([, chain]) => Array.isArray(chain) && chain.some(retired)).map(([model]) => model));
89
+ const desktopProfile = record(record(config.claudeCode)?.desktopProfile);
90
+ const desktopAssignments = record(desktopProfile?.assignments);
91
+ const desktopDefaults = record(desktopProfile?.defaults);
92
+ const retiredDesktopRoute = Object.keys(desktopAssignments ?? {}).some(retired)
93
+ || Object.values(desktopDefaults ?? {}).some(retired);
94
+ if (retired(config.injectionModel)) {
95
+ delete config.syncCodexSubagentDefaults;
96
+ delete config.injectionEffort;
97
+ }
98
+ for (const [key, value] of Object.entries(config)) {
99
+ if (key === "providers" || !/model|provider|combo|routing|claudeCode|fallback|redirect|compaction|autoReview|advisor|tokenGuardian|images|codexRoleTiers|sidecar|shadowCallIntercept/i.test(key)) continue;
100
+ const next = prune(value, key === "claudeCode");
101
+ if (next === undefined) delete config[key];
102
+ else config[key] = next;
103
+ }
104
+ // Desktop defaults require a member of their family, or null for an empty family.
105
+ // Repair only profiles affected by this migration; unrelated invalid input still degrades.
106
+ if (retiredDesktopRoute && desktopAssignments && desktopDefaults) {
107
+ for (const family of DESKTOP_FAMILIES) {
108
+ const current = desktopDefaults[family];
109
+ if (typeof current === "string" && record(desktopAssignments[current])?.family === family) continue;
110
+ desktopDefaults[family] = Object.keys(desktopAssignments)
111
+ .filter(route => record(desktopAssignments[route])?.family === family).sort()[0] ?? null;
112
+ }
113
+ }
114
+ if (providers) {
115
+ for (const provider of Object.values(providers)) {
116
+ const row = record(provider);
117
+ if (!row) continue;
118
+ for (const key of ["defaultModel", "autoReviewModel", "autoReviewModelOverrides"]) {
119
+ const next = prune(row[key], key === "autoReviewModelOverrides");
120
+ if (next === undefined) delete row[key];
121
+ else row[key] = next;
122
+ }
123
+ }
124
+ }
125
+ const fallbacks = record(config.subagentModelFallbackByModel);
126
+ if (fallbacks) {
127
+ for (const [model, chain] of Object.entries(fallbacks)) {
128
+ if (depletedFallbacks.has(model) && Array.isArray(chain) && chain.length === 0) delete fallbacks[model];
129
+ }
130
+ }
131
+ if (changed && !warned) {
132
+ warned = true;
133
+ console.warn("opencode-free support was removed because every free model now returns 403 permission_denied under OpenCode's upstream free-tier gate (#7255); the entry and dependent references are disabled. Use opencode-go, or another provider such as opencode-zen or openrouter.");
134
+ }
135
+ }
@@ -0,0 +1,14 @@
1
+ import * as z from "zod/v4";
2
+ import { ACCOUNT_PAUSE_REASONS } from "../../types/account-auto-pause";
3
+
4
+ export const accountAutoPausePolicySchema = z.object({
5
+ enabled: z.boolean().optional(),
6
+ consecutiveFailures: z.number().int().min(1).max(100).optional(),
7
+ }).strict();
8
+ export const accountPauseDetailsSchema = z.object({
9
+ reason: z.enum(ACCOUNT_PAUSE_REASONS),
10
+ pausedAt: z.number().int().nonnegative(),
11
+ }).strict();
12
+ export const accountPauseStateSchema = z.record(z.string(), z.object({
13
+ revision: z.string(), details: accountPauseDetailsSchema.optional(),
14
+ }).strict());