@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -26,6 +26,11 @@ import {
26
26
  } from "../providers/fastwire";
27
27
  import { openaiChatCompletionsUrl } from "./openai-chat-url";
28
28
  import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
29
+ import {
30
+ isXaiSchemaTarget,
31
+ lookupLocalJsonPointer,
32
+ normalizeXaiToolParameters,
33
+ } from "./xai-tool-schema";
29
34
  import {
30
35
  isTranslatorBudgetExceededError,
31
36
  retainTranslatedEventBatch,
@@ -922,11 +927,50 @@ function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
922
927
  || baseUrl === "https://opencode.ai/zen/go/v1";
923
928
  }
924
929
 
925
- function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
930
+ /** Azure Model Router (and Gemini-in-the-pool) 400s Codex MCP schemas whose root is a union. */
931
+ const AZURE_CHAT_FORBIDDEN_ROOT_KEYS = ["oneOf", "anyOf", "allOf", "enum", "const", "not"] as const;
932
+
933
+ function isAzureOpenAiChatTarget(provider: OcxProviderConfig): boolean {
934
+ try {
935
+ const host = new URL(provider.baseUrl).hostname.toLowerCase();
936
+ return host.endsWith(".openai.azure.com")
937
+ || host.endsWith(".cognitiveservices.azure.com")
938
+ || host.endsWith(".services.ai.azure.com")
939
+ || host.endsWith(".ai.azure.com");
940
+ } catch {
941
+ return false;
942
+ }
943
+ }
944
+
945
+ /**
946
+ * Azure Foundry Model Router validates every function schema against the strictest model in
947
+ * the pool (Gemini-shaped): root must be {type:"object"} with no oneOf/anyOf/allOf/enum/
948
+ * const/not. Codex App MCP tools such as mcp__codex_app__automation_update ship a root
949
+ * union, which 400s the whole turn. Flatten like Zen, then strip leftover forbidden keys.
950
+ */
951
+ function sanitizeAzureChatToolParameters(parameters: unknown): Record<string, unknown> {
952
+ const root = ensureZenRootObjectSchema(parameters);
953
+ for (const key of AZURE_CHAT_FORBIDDEN_ROOT_KEYS) delete root[key];
954
+ root.type = "object";
955
+ if (!root.properties || typeof root.properties !== "object" || Array.isArray(root.properties)) {
956
+ root.properties = {};
957
+ }
958
+ return root;
959
+ }
960
+
961
+ // Moonshot validates function schemas against a draft-07 reading of `$ref`, where the
962
+ // keyword stands alone and siblings are ignored. It rejects the whole request rather
963
+ // than ignoring them: "not a valid moonshot flavored json schema ... when using $ref,
964
+ // type should be defined in the referenced schema instead of the parent schema".
965
+ const MOONSHOT_SCHEMA_HOSTNAMES = new Set([
966
+ "api.kimi.com",
967
+ "api.moonshot.ai",
968
+ "api.moonshot.cn",
969
+ ]);
970
+
971
+ function isMoonshotSchemaTarget(provider: OcxProviderConfig): boolean {
926
972
  try {
927
- // Public api.x.ai accepts native root object unions. Only the Grok CLI proxy
928
- // 400s on a root oneOf/anyOf, so flattening/omitting is scoped to that host.
929
- return new URL(provider.baseUrl).hostname === "cli-chat-proxy.grok.com";
973
+ return MOONSHOT_SCHEMA_HOSTNAMES.has(new URL(provider.baseUrl).hostname);
930
974
  } catch {
931
975
  return false;
932
976
  }
@@ -964,263 +1008,248 @@ function isXaiObjectSchema(value: unknown): value is Record<string, unknown> {
964
1008
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
965
1009
  }
966
1010
 
967
- function stringRequiredFields(value: unknown): string[] {
968
- return Array.isArray(value)
969
- ? value.filter((item): item is string => typeof item === "string")
970
- : [];
1011
+ /**
1012
+ * JSON Schema 2020-12 makes `$ref` an in-place applicator: siblings stay in force and are
1013
+ * combined with the referenced schema. Moonshot enforces the older draft-07 reading where
1014
+ * `$ref` must stand alone, and 400s the entire request when a node carries both. Codex's own
1015
+ * deferred tool catalog emits exactly that shape (zod-to-json-schema deduplicates into
1016
+ * `$defs.__schema*` nodes that keep `type`/`minLength`/`format` beside the `$ref`), so the
1017
+ * schema is not something a user can fix from configuration — see issue #2673.
1018
+ *
1019
+ * Inline the referenced schema underneath the node's own keywords, which is what 2020-12 says
1020
+ * the node means, then drop `$ref`. Constraints reach the model instead of being stripped.
1021
+ * The `$defs` bag is preserved: a bare `$ref` (no siblings) is already legal for Moonshot and
1022
+ * is left pointing at its definition rather than expanded, which keeps recursive schemas finite.
1023
+ */
1024
+ function moonshotRefTargetKeys(node: Record<string, unknown>): string[] {
1025
+ return Object.keys(node).filter(key => key !== "$ref");
971
1026
  }
972
1027
 
973
- /** Variant keys the merger can keep. Anything else is refused, not silently dropped. */
974
- const XAI_VARIANT_MERGE_KEYS = new Set([
975
- "type",
976
- "properties",
977
- "required",
978
- "additionalProperties",
979
- "description",
980
- "title",
981
- "$comment",
982
- "$defs",
983
- "definitions",
984
- ]);
1028
+ /**
1029
+ * Inlining duplicates the target, so a schema referencing one large definition from many
1030
+ * sibling-carrying nodes can multiply. Bound the total expansions and fall back to a bare
1031
+ * `$ref` once the budget is spent: still valid for Moonshot, just without the node's own
1032
+ * narrowing keywords. Mirrors the node budget in google-tool-schema.ts.
1033
+ */
1034
+ const MOONSHOT_MAX_REF_EXPANSIONS = 512;
985
1035
 
986
- function decodeJsonPointerToken(token: string): string {
987
- return token.replace(/~1/g, "/").replace(/~0/g, "~");
988
- }
1036
+ /**
1037
+ * Expansion count alone does not bound the walk: a deeply nested ref-free schema, or one
1038
+ * large definition repeated across many nodes, still recurses to exhaustion or amplifies the
1039
+ * emitted output. Depth and node budgets close both, and mirror google-tool-schema.ts.
1040
+ */
1041
+ const MOONSHOT_MAX_SCHEMA_DEPTH = 64;
1042
+ const MOONSHOT_MAX_SCHEMA_NODES = 4_096;
989
1043
 
990
- function lookupLocalJsonPointer(root: unknown, ref: string): unknown {
991
- if (ref === "#" || ref === "#/") return root;
992
- if (!ref.startsWith("#/")) return undefined;
993
- let current: unknown = root;
994
- for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) {
995
- if (!isXaiObjectSchema(current) || !Object.hasOwn(current, token)) return undefined;
996
- current = current[token];
1044
+ /**
1045
+ * Assertion keywords whose meaning under a `$ref` is CONJUNCTION, not replacement. A node
1046
+ * carrying `required: ["b"]` beside a target requiring `["a"]` means both are required;
1047
+ * letting the sibling win emitted a schema that no longer described the tool.
1048
+ */
1049
+ function unionRequired(target: unknown, sibling: unknown): unknown {
1050
+ if (!Array.isArray(target) || !Array.isArray(sibling)) return sibling;
1051
+ const seen = new Set<unknown>();
1052
+ const out: unknown[] = [];
1053
+ for (const name of [...target, ...sibling]) {
1054
+ if (seen.has(name)) continue;
1055
+ seen.add(name);
1056
+ out.push(name);
997
1057
  }
998
- return current;
1058
+ return out;
999
1059
  }
1000
1060
 
1001
- /** Resolve local `#/` `$ref`s. Unresolvable or cyclic refs return undefined. */
1002
- function resolveXaiSchemaRefs(
1003
- schema: unknown,
1004
- root: Record<string, unknown>,
1005
- stack: Set<string> = new Set(),
1006
- ): unknown | undefined {
1007
- if (!isXaiObjectSchema(schema)) return schema;
1008
- if (typeof schema.$ref === "string") {
1009
- const ref = schema.$ref;
1010
- if (stack.has(ref)) return undefined;
1011
- const target = lookupLocalJsonPointer(root, ref);
1012
- if (target === undefined) return undefined;
1013
- stack.add(ref);
1014
- const resolvedTarget = resolveXaiSchemaRefs(target, root, stack);
1015
- stack.delete(ref);
1016
- if (resolvedTarget === undefined) return undefined;
1017
- const rest: Record<string, unknown> = { ...schema };
1018
- delete rest.$ref;
1019
- if (Object.keys(rest).length === 0) return resolvedTarget;
1020
- const resolvedRest = resolveXaiSchemaRefs(rest, root, stack);
1021
- if (resolvedRest === undefined || !isXaiObjectSchema(resolvedTarget) || !isXaiObjectSchema(resolvedRest)) {
1022
- return undefined;
1023
- }
1024
- return composeXaiObjectSchemas(resolvedTarget, resolvedRest);
1025
- }
1026
-
1027
- const resolved: Record<string, unknown> = {};
1028
- for (const [key, value] of Object.entries(schema)) {
1029
- if ((key === "oneOf" || key === "anyOf") && Array.isArray(value)) {
1030
- const items: unknown[] = [];
1031
- for (const item of value) {
1032
- const next = resolveXaiSchemaRefs(item, root, stack);
1033
- if (next === undefined) return undefined;
1034
- items.push(next);
1035
- }
1036
- resolved[key] = items;
1037
- continue;
1038
- }
1039
- if (key === "properties" && isXaiObjectSchema(value)) {
1040
- const properties: Record<string, unknown> = {};
1041
- for (const [name, property] of Object.entries(value)) {
1042
- const next = resolveXaiSchemaRefs(property, root, stack);
1043
- if (next === undefined) return undefined;
1044
- properties[name] = next;
1045
- }
1046
- resolved[key] = properties;
1047
- continue;
1048
- }
1049
- resolved[key] = value;
1050
- }
1051
- return resolved;
1052
- }
1061
+ /**
1062
+ * Keywords whose values are DATA, not schemas.
1063
+ *
1064
+ * Recursing into them rewrote user data: an `enum` listing a literal object that happens
1065
+ * to carry a `"$ref"` string had that key stripped as if it were a schema reference, so a
1066
+ * value the tool declared as legal silently changed shape. These are copied through.
1067
+ */
1068
+ const MOONSHOT_DATA_VALUED_KEYWORDS = new Set(["enum", "const", "default", "examples"]);
1053
1069
 
1054
- function xaiVariantIsConcreteObject(variant: Record<string, unknown>): boolean {
1055
- if (variant.type !== undefined && variant.type !== "object") return false;
1056
- return Object.keys(variant).every(key => XAI_VARIANT_MERGE_KEYS.has(key));
1057
- }
1070
+ /**
1071
+ * Numeric assertions whose intersection is a bound, and which direction tightens.
1072
+ *
1073
+ * `$ref` under 2020-12 is an in-place applicator: the node and its target BOTH apply, so
1074
+ * the emitted schema must be their INTERSECTION. The previous code overwrote the target
1075
+ * with the node and called that "the narrower reading", which holds only when the node
1076
+ * happens to be narrower. A node declaring `minLength: 1` beside a target declaring
1077
+ * `minLength: 5` shipped `minLength: 1` - a contract weaker than either side asked for,
1078
+ * emitted silently, which is the same failure mode the `required` composition fixed for
1079
+ * set-valued keywords.
1080
+ *
1081
+ * "max" means the surviving value is the larger of the two (lower bounds), "min" the
1082
+ * smaller (upper bounds). A keyword absent from this table keeps the overwrite: for
1083
+ * `type`, `format`, `description` and friends there is no ordering to intersect along,
1084
+ * and the node is the more specific statement.
1085
+ */
1086
+ const MOONSHOT_BOUND_KEYWORDS: Record<string, "max" | "min"> = {
1087
+ minLength: "max",
1088
+ minItems: "max",
1089
+ minProperties: "max",
1090
+ minimum: "max",
1091
+ exclusiveMinimum: "max",
1092
+ maxLength: "min",
1093
+ maxItems: "min",
1094
+ maxProperties: "min",
1095
+ maximum: "min",
1096
+ exclusiveMaximum: "min",
1097
+ };
1058
1098
 
1059
- function variantProperties(variant: Record<string, unknown>): Record<string, unknown> {
1060
- return isXaiObjectSchema(variant.properties) ? variant.properties : {};
1099
+ /**
1100
+ * Intersect one numeric bound. Either side being absent or non-finite yields the other,
1101
+ * because an unstated bound constrains nothing - returning `undefined` there would drop
1102
+ * a constraint the remaining side genuinely made.
1103
+ */
1104
+ function intersectBound(target: unknown, sibling: unknown, direction: "max" | "min"): unknown {
1105
+ const a = typeof target === "number" && Number.isFinite(target) ? target : null;
1106
+ const b = typeof sibling === "number" && Number.isFinite(sibling) ? sibling : null;
1107
+ if (a === null) return b === null ? sibling : sibling;
1108
+ if (b === null) return target;
1109
+ return direction === "max" ? Math.max(a, b) : Math.min(a, b);
1061
1110
  }
1062
1111
 
1063
1112
  /**
1064
- * Independent per-property anyOf is lossless only when every property name exists
1065
- * on every variant (absence is meaningful under xAI's default additionalProperties:
1066
- * false, and promoting a branch-local key also tightens explicit-true variants)
1067
- * and at most one of those shared properties has a conflicting schema.
1113
+ * Compose two `properties` maps. A property named in BOTH the referenced target and the
1114
+ * node is the same conjunction problem `required` had: letting the sibling win discards
1115
+ * the target's constraints for that member. Merge the two member schemas so neither side
1116
+ * loses its keywords. Shared member bounds are the same conjunction one level down,
1117
+ * and nested object members recurse through this helper instead of replacing the target.
1068
1118
  */
1069
- function xaiPropertyMergeIsLossless(variants: Record<string, unknown>[]): boolean {
1070
- const names = new Set<string>();
1071
- const props = variants.map(variant => {
1072
- const properties = variantProperties(variant);
1073
- for (const name of Object.keys(properties)) names.add(name);
1074
- return properties;
1075
- });
1076
- let schemaConflicts = 0;
1077
- for (const name of names) {
1078
- const values = props.map(property => property[name]);
1079
- if (values.some(value => value === undefined)) return false;
1080
- if (values.some(value => JSON.stringify(value) !== JSON.stringify(values[0]))) schemaConflicts += 1;
1119
+ function composeProperties(
1120
+ target: Record<string, unknown>,
1121
+ sibling: Record<string, unknown>,
1122
+ ): Record<string, unknown> {
1123
+ const combined: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
1124
+ for (const [name, sub] of Object.entries(target)) combined[name] = sub;
1125
+ for (const [name, sub] of Object.entries(sibling)) {
1126
+ const existing = combined[name];
1127
+ if (isXaiObjectSchema(existing) && isXaiObjectSchema(sub)) {
1128
+ const member: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
1129
+ for (const [k, v] of Object.entries(existing)) member[k] = v;
1130
+ for (const [k, v] of Object.entries(sub)) {
1131
+ if (k === "required") {
1132
+ member[k] = unionRequired(member[k], v);
1133
+ continue;
1134
+ }
1135
+ if (k === "properties" && isXaiObjectSchema(member[k]) && isXaiObjectSchema(v)) {
1136
+ member[k] = composeProperties(member[k] as Record<string, unknown>, v);
1137
+ continue;
1138
+ }
1139
+ const boundDirection = MOONSHOT_BOUND_KEYWORDS[k];
1140
+ if (boundDirection && k in member) {
1141
+ member[k] = intersectBound(member[k], v, boundDirection);
1142
+ continue;
1143
+ }
1144
+ member[k] = v;
1145
+ }
1146
+ combined[name] = member;
1147
+ continue;
1148
+ }
1149
+ combined[name] = sub;
1081
1150
  }
1082
- return schemaConflicts <= 1;
1083
- }
1084
-
1085
- function xaiRequiredSetsMatch(variants: Record<string, unknown>[]): boolean {
1086
- const serialized = variants.map(variant => [...stringRequiredFields(variant.required)].sort().join("\0"));
1087
- return serialized.every(value => value === serialized[0]);
1151
+ return combined;
1088
1152
  }
1089
1153
 
1090
- function mergeXaiAdditionalProperties(
1091
- variants: Record<string, unknown>[],
1092
- ): { ok: true; value?: unknown } | { ok: false } {
1093
- const values = variants.map(variant => variant.additionalProperties);
1094
- const explicit = values.filter(value => value !== undefined);
1095
- if (explicit.length === 0) return { ok: true };
1096
- if (explicit.length !== values.length) return { ok: false };
1097
- const hasFalse = explicit.some(value => value === false);
1098
- const permissive = explicit.filter(value => value !== false);
1099
- if (hasFalse && permissive.length > 0) return { ok: false };
1100
- if (hasFalse) return { ok: true, value: false };
1101
- const unique: unknown[] = [];
1102
- const seen = new Set<string>();
1103
- for (const value of permissive) {
1104
- const key = JSON.stringify(value);
1105
- if (seen.has(key)) continue;
1106
- seen.add(key);
1107
- unique.push(value);
1108
- }
1109
- if (unique.length !== 1) return { ok: false };
1110
- return { ok: true, value: unique[0] };
1154
+ interface MoonshotNormalizeState {
1155
+ activeRefs: Set<string>;
1156
+ remainingExpansions: number;
1157
+ remainingNodes: number;
1111
1158
  }
1112
1159
 
1113
- /** Compose root siblings into a branch so properties/required are not overwritten. */
1114
- function composeXaiObjectSchemas(
1115
- inherited: Record<string, unknown>,
1116
- branch: Record<string, unknown>,
1117
- ): Record<string, unknown> {
1118
- const composed: Record<string, unknown> = { ...inherited, ...branch };
1119
- const inheritedProps = isXaiObjectSchema(inherited.properties) ? inherited.properties : undefined;
1120
- const branchProps = isXaiObjectSchema(branch.properties) ? branch.properties : undefined;
1121
- if (inheritedProps || branchProps) {
1122
- const properties: Record<string, unknown> = { ...(inheritedProps ?? {}) };
1123
- for (const [name, value] of Object.entries(branchProps ?? {})) {
1124
- const inheritedValue = inheritedProps?.[name];
1125
- properties[name] = inheritedValue !== undefined && JSON.stringify(inheritedValue) !== JSON.stringify(value)
1126
- ? { allOf: [inheritedValue, value] }
1127
- : value;
1128
- }
1129
- composed.properties = properties;
1160
+ function normalizeMoonshotSchemaNode(
1161
+ node: unknown,
1162
+ root: Record<string, unknown>,
1163
+ state: MoonshotNormalizeState,
1164
+ depth = 0,
1165
+ ): unknown {
1166
+ if (Array.isArray(node)) {
1167
+ if (depth >= MOONSHOT_MAX_SCHEMA_DEPTH) return [];
1168
+ return node.map(item => normalizeMoonshotSchemaNode(item, root, state, depth + 1));
1130
1169
  }
1131
- const required = [...new Set([
1132
- ...stringRequiredFields(inherited.required),
1133
- ...stringRequiredFields(branch.required),
1134
- ])];
1135
- if (required.length > 0) composed.required = required;
1136
- else delete composed.required;
1137
- return composed;
1138
- }
1170
+ if (!isXaiObjectSchema(node)) return node;
1139
1171
 
1140
- function expandXaiRootObjectSchemas(schema: unknown): Record<string, unknown>[] | undefined {
1141
- if (!isXaiObjectSchema(schema)) return undefined;
1142
- const compositionKey = ["oneOf", "anyOf"].find(key => Array.isArray(schema[key]));
1143
- if (!compositionKey) {
1144
- if (schema.type !== undefined && schema.type !== "object") return undefined;
1145
- return [{ ...schema, type: "object" }];
1146
- }
1172
+ // Fail closed for this node rather than emitting a partially weakened schema: an empty
1173
+ // object is the one shape that asserts nothing it cannot back up.
1174
+ if (depth >= MOONSHOT_MAX_SCHEMA_DEPTH || state.remainingNodes <= 0) return {};
1175
+ state.remainingNodes -= 1;
1147
1176
 
1148
- const siblings = Object.fromEntries(Object.entries(schema).filter(([key]) => key !== compositionKey));
1149
- const branches = schema[compositionKey];
1150
- if (!Array.isArray(branches)) return undefined;
1151
- const expanded: Record<string, unknown>[] = [];
1152
- for (const branch of branches) {
1153
- const variants = expandXaiRootObjectSchemas(branch);
1154
- if (!variants) return undefined;
1155
- for (const variant of variants) expanded.push(composeXaiObjectSchemas(siblings, variant));
1156
- }
1157
- return expanded.length > 0 ? expanded : undefined;
1158
- }
1177
+ const ref = node.$ref;
1178
+ const hasSiblings = moonshotRefTargetKeys(node).length > 0;
1159
1179
 
1160
- function mergeXaiPropertySchemas(values: unknown[]): unknown {
1161
- const unique: unknown[] = [];
1162
- const serialized = new Set<string>();
1163
- for (const value of values) {
1164
- const key = JSON.stringify(value);
1165
- if (serialized.has(key)) continue;
1166
- serialized.add(key);
1167
- unique.push(value);
1168
- }
1169
- return unique.length === 1 ? unique[0] : { anyOf: unique };
1170
- }
1180
+ if (typeof ref === "string" && hasSiblings) {
1181
+ // A cycle cannot be inlined. Keeping the bare `$ref` is the lossy-but-valid fallback:
1182
+ // Moonshot accepts it, and the alternative (dropping the ref) would erase the recursion.
1183
+ if (state.activeRefs.has(ref) || state.remainingExpansions <= 0) return { $ref: ref };
1171
1184
 
1172
- /**
1173
- * The Grok CLI proxy rejects a function parameter schema whose root remains oneOf/anyOf.
1174
- * Flatten only when the merge is lossless: local $refs resolve, every variant is a concrete
1175
- * object whose keys we can preserve, required sets match, additionalProperties does not change
1176
- * meaning, every property name exists on every variant, and at most one property schema
1177
- * differs. Otherwise omit the tool rather than emit a weaker schema.
1178
- */
1179
- function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown> | undefined {
1180
- if (!isXaiObjectSchema(parameters)) return undefined;
1181
- const resolved = resolveXaiSchemaRefs(parameters, parameters);
1182
- if (!isXaiObjectSchema(resolved)) return undefined;
1183
-
1184
- const normalizedRoot = { ...resolved };
1185
- delete normalizedRoot.$schema;
1186
-
1187
- const variants = expandXaiRootObjectSchemas(normalizedRoot);
1188
- if (!variants) return undefined;
1189
- if (variants.length === 1) {
1190
- return xaiVariantIsConcreteObject(variants[0]) ? variants[0] : undefined;
1191
- }
1192
- if (!variants.every(xaiVariantIsConcreteObject) || !xaiRequiredSetsMatch(variants)) return undefined;
1193
- const additionalProperties = mergeXaiAdditionalProperties(variants);
1194
- if (!additionalProperties.ok) return undefined;
1195
- if (!xaiPropertyMergeIsLossless(variants)) return undefined;
1196
-
1197
- const metadata = Object.fromEntries(Object.entries(normalizedRoot).filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
1198
- delete metadata.properties;
1199
- delete metadata.required;
1200
- delete metadata.additionalProperties;
1201
-
1202
- const propertyValues = new Map<string, unknown[]>();
1203
- for (const variant of variants) {
1204
- if (!variant.properties || typeof variant.properties !== "object" || Array.isArray(variant.properties)) continue;
1205
- for (const [name, value] of Object.entries(variant.properties as Record<string, unknown>)) {
1206
- const values = propertyValues.get(name) ?? [];
1207
- values.push(value);
1208
- propertyValues.set(name, values);
1185
+ const target = lookupLocalJsonPointer(root, ref);
1186
+ if (isXaiObjectSchema(target)) {
1187
+ state.remainingExpansions -= 1;
1188
+ state.activeRefs.add(ref);
1189
+ const resolvedTarget = normalizeMoonshotSchemaNode(target, root, state, depth + 1);
1190
+ state.activeRefs.delete(ref);
1191
+ const merged: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
1192
+ if (isXaiObjectSchema(resolvedTarget)) {
1193
+ for (const [key, value] of Object.entries(resolvedTarget)) merged[key] = value;
1194
+ }
1195
+ // "Alongside the target" is conjunction, not replacement. For most keywords the node
1196
+ // narrows the target and overwriting is the narrower reading, but `required` and
1197
+ // `properties` are set-valued: letting the sibling win DROPPED the target's own
1198
+ // members, so a tool requiring `a` beside a node requiring `b` shipped requiring only
1199
+ // `b`. Those two compose; everything else keeps the narrowing overwrite.
1200
+ for (const [key, value] of Object.entries(node)) {
1201
+ if (key === "$ref") continue;
1202
+ if (MOONSHOT_DATA_VALUED_KEYWORDS.has(key)) {
1203
+ merged[key] = value;
1204
+ continue;
1205
+ }
1206
+ const normalized = normalizeMoonshotSchemaNode(value, root, state, depth + 1);
1207
+ if (key === "required") {
1208
+ merged[key] = unionRequired(merged[key], normalized);
1209
+ continue;
1210
+ }
1211
+ if (key === "properties" && isXaiObjectSchema(merged[key]) && isXaiObjectSchema(normalized)) {
1212
+ merged[key] = composeProperties(merged[key] as Record<string, unknown>, normalized);
1213
+ continue;
1214
+ }
1215
+ // Numeric bounds intersect rather than overwrite: both the node and its target
1216
+ // apply, so the surviving bound is the stricter of the two in whichever direction
1217
+ // that keyword tightens.
1218
+ const boundDirection = MOONSHOT_BOUND_KEYWORDS[key];
1219
+ if (boundDirection && key in merged) {
1220
+ merged[key] = intersectBound(merged[key], normalized, boundDirection);
1221
+ continue;
1222
+ }
1223
+ merged[key] = normalized;
1224
+ }
1225
+ return merged;
1209
1226
  }
1227
+
1228
+ // Unresolvable pointer: a remote ref, a malformed path, or a non-object target. Dropping
1229
+ // the ref and keeping the siblings silently discards whatever the reference constrained,
1230
+ // which is the one outcome we cannot detect downstream. A bare `$ref` is lossy in the
1231
+ // other direction - it loses the node's own keywords - but it preserves the identity of
1232
+ // what was asked for, and Moonshot accepts it.
1233
+ return { $ref: ref };
1210
1234
  }
1211
1235
 
1212
- const properties = Object.fromEntries(
1213
- [...propertyValues].map(([name, values]) => [name, mergeXaiPropertySchemas(values)]),
1214
- );
1215
- const required = stringRequiredFields(variants[0]?.required);
1236
+ const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
1237
+ for (const [key, value] of Object.entries(node)) {
1238
+ out[key] = key === "$ref" || MOONSHOT_DATA_VALUED_KEYWORDS.has(key)
1239
+ ? value
1240
+ : normalizeMoonshotSchemaNode(value, root, state, depth + 1);
1241
+ }
1242
+ return out;
1243
+ }
1216
1244
 
1217
- return {
1218
- ...metadata,
1219
- type: "object",
1220
- properties,
1221
- ...(required.length > 0 ? { required } : {}),
1222
- ...("value" in additionalProperties ? { additionalProperties: additionalProperties.value } : {}),
1223
- };
1245
+ function normalizeMoonshotToolParameters(parameters: unknown): Record<string, unknown> {
1246
+ const rooted = ensureRootObjectType(parameters);
1247
+ const normalized = normalizeMoonshotSchemaNode(rooted, rooted, {
1248
+ activeRefs: new Set<string>(),
1249
+ remainingExpansions: MOONSHOT_MAX_REF_EXPANSIONS,
1250
+ remainingNodes: MOONSHOT_MAX_SCHEMA_NODES,
1251
+ });
1252
+ return isXaiObjectSchema(normalized) ? normalized : rooted;
1224
1253
  }
1225
1254
 
1226
1255
  function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
@@ -1228,10 +1257,14 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
1228
1257
  const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
1229
1258
  if (tools.length === 0) return undefined;
1230
1259
  const xaiTarget = isXaiSchemaTarget(provider);
1260
+ const moonshotTarget = !xaiTarget && isMoonshotSchemaTarget(provider);
1231
1261
  const formatted = tools.flatMap(t => {
1232
- const parameters = stripResponsesOnlyEncryptedMarker(xaiTarget
1262
+ const normalized = xaiTarget
1233
1263
  ? normalizeXaiToolParameters(t.parameters)
1234
- : ensureRootObjectType(t.parameters));
1264
+ : moonshotTarget
1265
+ ? normalizeMoonshotToolParameters(t.parameters)
1266
+ : ensureRootObjectType(t.parameters);
1267
+ const parameters = stripResponsesOnlyEncryptedMarker(normalized);
1235
1268
 
1236
1269
  if (parameters === undefined) return [];
1237
1270
  return [{
@@ -1249,17 +1282,22 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
1249
1282
 
1250
1283
  function toolsToChatFormatForProvider(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
1251
1284
  const base = toolsToChatFormat(parsed, provider);
1252
- if (!base || !shouldSanitizeZenToolParameters(provider)) return base;
1285
+ const azureChat = isAzureOpenAiChatTarget(provider);
1286
+ const zenChat = shouldSanitizeZenToolParameters(provider);
1287
+ if (!base || (!zenChat && !azureChat)) return base;
1253
1288
  return base.map(tool => {
1254
1289
  if (!tool || typeof tool !== "object") return tool;
1255
1290
  const functionDef = (tool as { function?: Record<string, unknown> }).function;
1256
1291
  if (!functionDef || typeof functionDef !== "object") return tool;
1292
+ const parameters = azureChat
1293
+ ? sanitizeAzureChatToolParameters(functionDef.parameters ?? {})
1294
+ : ensureZenRootObjectSchema(functionDef.parameters ?? {});
1295
+ const nextFunction: Record<string, unknown> = { ...functionDef, parameters };
1296
+ // strict: true plus a flattened schema is rejected by Gemini-in-the-pool routers.
1297
+ if (azureChat) delete nextFunction.strict;
1257
1298
  return {
1258
1299
  ...tool,
1259
- function: {
1260
- ...functionDef,
1261
- parameters: ensureZenRootObjectSchema(functionDef.parameters ?? {}),
1262
- },
1300
+ function: nextFunction,
1263
1301
  };
1264
1302
  });
1265
1303
  }
@@ -1932,8 +1970,28 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
1932
1970
  const choice = rawChoice;
1933
1971
  if (choice.finish_reason === "error") return [upstreamErrorEvent(choice.error, usage)];
1934
1972
  if (!choice.message) return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
1973
+ // `!choice.message` splits this input class on TRUTHINESS, not on shape: `null` and `0` fail
1974
+ // closed here, while `"text"`, `true` and `[{...}]` pass and every property read below yields
1975
+ // `undefined` — so a choice claiming an assistant message completed as a SUCCESSFUL EMPTY
1976
+ // turn, stranding any tool call it claimed. The one line above already validates the choice
1977
+ // container this way; its message was left on a truthiness test.
1978
+ //
1979
+ // Every non-record is rejected, arrays included. The google adapter does carve out `[]` for
1980
+ // `content`, but that carve-out is specific to a protobuf-derived wire where a repeated
1981
+ // field can spell an empty message — and `content` is genuinely an ARRAY of blocks there.
1982
+ // `message` is a record on a plain-JSON wire that already has `{}`, so importing the
1983
+ // exception would be an analogy rather than evidence. `[{"content":"…"}]` is the case that
1984
+ // matters: it discards a complete answer, #2232's `content: [{ parts: [...] }]` one adapter over.
1985
+ //
1986
+ // Read through `unknown` rather than the declared type: `choices` is a cast over wire data,
1987
+ // so its `message?: Record<string, unknown>` is an assertion the upstream never made, and
1988
+ // narrowing against it is what let the missing check look type-safe.
1989
+ const rawMessage: unknown = choice.message;
1990
+ if (!isRecord(rawMessage)) {
1991
+ return [invalidChoicesEvent(usage)];
1992
+ }
1935
1993
 
1936
- const msg = choice.message;
1994
+ const msg = rawMessage as Record<string, unknown>;
1937
1995
  const reasoningText = reasoningTextFrom(msg);
1938
1996
  if (reasoningText !== undefined) events.push({ type: "reasoning_raw_delta", text: reasoningText });
1939
1997
  if (typeof msg.content === "string") events.push({ type: "text_delta", text: msg.content });