@coseung2/opencodex 2.8.0-cs.16 → 2.8.0-cs.17

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 (52) hide show
  1. package/gui/dist/assets/{index-BZGMtkmp.js → index-Ch-99jy3.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/base.ts +12 -0
  5. package/src/adapters/kiro-calibration.ts +83 -0
  6. package/src/adapters/kiro-constants.ts +11 -2
  7. package/src/adapters/kiro-errors.ts +11 -0
  8. package/src/adapters/kiro-events.ts +19 -1
  9. package/src/adapters/kiro-thinking.ts +18 -2
  10. package/src/adapters/kiro-tools.ts +12 -3
  11. package/src/adapters/kiro.ts +300 -78
  12. package/src/adapters/openai-chat.ts +1 -42
  13. package/src/adapters/openai-responses.ts +93 -14
  14. package/src/adapters/xai-schema-analysis.ts +78 -0
  15. package/src/adapters/xai-tool-schema.ts +274 -0
  16. package/src/adapters/xai-web-search.ts +138 -0
  17. package/src/bridge.ts +61 -6
  18. package/src/codex/catalog/effort.ts +4 -2
  19. package/src/codex/catalog/metadata.ts +38 -9
  20. package/src/codex/catalog/parsing.ts +17 -2
  21. package/src/codex/catalog/provider-fetch.ts +9 -3
  22. package/src/codex/catalog/sync.ts +8 -5
  23. package/src/codex/data/upstream-models.json +169 -0
  24. package/src/grok/inject.ts +1 -1
  25. package/src/lib/token-estimate.ts +42 -38
  26. package/src/lib/translator-budget.ts +34 -0
  27. package/src/oauth/index.ts +10 -4
  28. package/src/oauth/kiro.ts +71 -6
  29. package/src/oauth/store.ts +3 -1
  30. package/src/oauth/types.ts +4 -0
  31. package/src/providers/derive.ts +7 -5
  32. package/src/providers/opencode-go-transport.ts +18 -0
  33. package/src/providers/registry.ts +34 -10
  34. package/src/providers/xai-transport.ts +10 -0
  35. package/src/responses/compaction.ts +8 -1
  36. package/src/responses/namespace-aliases.ts +56 -0
  37. package/src/responses/parser.ts +12 -0
  38. package/src/responses/reasoning-envelope.ts +9 -1
  39. package/src/responses/snapshot-policy.ts +108 -0
  40. package/src/responses/state.ts +23 -10
  41. package/src/responses/turn-termination.ts +108 -0
  42. package/src/responses/xai-custom-tool-compat.ts +237 -0
  43. package/src/server/grok-responses-snapshot-repair.ts +338 -0
  44. package/src/server/index.ts +2 -1
  45. package/src/server/relay-eager.ts +1 -0
  46. package/src/server/responses/core.ts +173 -13
  47. package/src/server/responses-image-gen-repair.ts +2 -2
  48. package/src/server/sse-payload-rewrite.ts +20 -3
  49. package/src/types.ts +10 -1
  50. package/src/usage/cost.ts +0 -0
  51. package/src/usage/expected-prices.ts +7 -0
  52. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
@@ -9,6 +9,7 @@ import { redactSecretString } from "../lib/redact";
9
9
  import { contentPartsToText } from "./image";
10
10
  import { neutralizeIdentity } from "./identity";
11
11
  import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
12
+ import { isXaiSchemaTarget, normalizeXaiToolParameters } from "./xai-tool-schema";
12
13
  import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
13
14
  import {
14
15
  isTranslatorBudgetExceededError,
@@ -360,16 +361,6 @@ function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
360
361
  || baseUrl === "https://opencode.ai/zen/go/v1";
361
362
  }
362
363
 
363
- const XAI_SCHEMA_BASE_URLS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
364
-
365
- function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
366
- try {
367
- return XAI_SCHEMA_BASE_URLS.has(new URL(provider.baseUrl).hostname);
368
- } catch {
369
- return false;
370
- }
371
- }
372
-
373
364
  function isKimiSchemaTarget(provider: OcxProviderConfig): boolean {
374
365
  try {
375
366
  return new URL(provider.baseUrl).hostname === "api.kimi.com";
@@ -429,38 +420,6 @@ function ensureKimiRootObjectType(parameters: unknown): Record<string, unknown>
429
420
  return { ...obj, type: "object" };
430
421
  }
431
422
 
432
- function expandXaiRootObjectSchemas(schema: unknown): Record<string, unknown>[] | undefined {
433
- if (!schema || typeof schema !== "object" || Array.isArray(schema)) return undefined;
434
- const obj = schema as Record<string, unknown>;
435
- const compositionKey = ["oneOf", "anyOf"].find(key => Array.isArray(obj[key]));
436
- if (!compositionKey) {
437
- if (obj.type !== undefined && obj.type !== "object") return undefined;
438
- return [{ ...obj, type: "object" }];
439
- }
440
-
441
- const siblings = Object.fromEntries(Object.entries(obj).filter(([key]) => key !== compositionKey));
442
- const branches = obj[compositionKey];
443
- if (!Array.isArray(branches)) return undefined;
444
- const expanded: Record<string, unknown>[] = [];
445
- for (const branch of branches) {
446
- const variants = expandXaiRootObjectSchemas(branch);
447
- if (!variants) return undefined;
448
- for (const variant of variants) expanded.push({ ...siblings, ...variant });
449
- }
450
- return expanded.length > 0 ? expanded : undefined;
451
- }
452
-
453
- function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown> | undefined {
454
- const variants = expandXaiRootObjectSchemas(parameters);
455
- if (!variants) return undefined;
456
- if (variants.length === 1) return variants[0];
457
- const root = parameters && typeof parameters === "object" && !Array.isArray(parameters)
458
- ? parameters as Record<string, unknown>
459
- : {};
460
- const metadata = Object.fromEntries(Object.entries(root).filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
461
- return { ...metadata, oneOf: variants };
462
- }
463
-
464
423
  function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
465
424
  if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
466
425
  const allowed = isAllowedToolChoice(parsed.options.toolChoice)
@@ -3,7 +3,13 @@ import type { IncomingMeta, ProviderAdapter } from "./base";
3
3
  import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage } from "../types";
4
4
  import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
5
5
  import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
6
+ import { isOpenCodeMuseResponses } from "../providers/opencode-go-transport";
7
+ import { isXaiResponsesDestination } from "../providers/xai-transport";
8
+ import { debugProviderDiagnostic } from "../lib/debug";
9
+ import { isXaiSchemaTarget, normalizeXaiToolParameters, XaiToolSchemaCompatibilityError } from "./xai-tool-schema";
10
+ import { normalizeXaiResponsesWebSearch } from "./xai-web-search";
6
11
  import { collectResponsesToolGroups } from "../responses/tool-groups";
12
+ import { lowerXaiResponsesCustomTools } from "../responses/xai-custom-tool-compat";
7
13
  import { decodeServerSentEvents } from "../lib/sse-decoder";
8
14
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
9
15
  import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
@@ -255,11 +261,8 @@ function normalizeConfiguredReasoningSummaryDelivery(
255
261
  * - Drops tool_search_call/tool_search_output input items
256
262
  * - Sets parallel_tool_calls to false
257
263
  */
258
- const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set(["muse-spark-1.3-contributor", "muse-spark-1.2-contributor"]);
259
-
260
- function stripMuseSparkUnsupportedWebSearchFields(body: unknown, modelId: unknown): unknown {
261
- if (!isPlainObject(body) || typeof modelId !== "string"
262
- || !MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body;
264
+ function stripMuseSparkUnsupportedWebSearchFields(body: unknown, modelId: unknown, responseUrl: string): unknown {
265
+ if (!isPlainObject(body) || !isOpenCodeMuseResponses(modelId, responseUrl)) return body;
263
266
  const rewrite = (tools: unknown[]) => {
264
267
  let changed = false;
265
268
  const next = tools.map(tool => {
@@ -389,12 +392,34 @@ function stripSparkCompatibility(body: unknown): unknown {
389
392
  : body;
390
393
  }
391
394
 
395
+ function stripXaiOAuthOnlyParams(body: unknown, provider: OcxProviderConfig): unknown {
396
+ if (provider.authMode !== "oauth" || !isXaiResponsesDestination(provider) || !isPlainObject(body)) return body;
397
+ let changed = false;
398
+ const next: Record<string, unknown> = { ...body };
399
+ // The Grok subscription gateway has no caller-owned Priority/Fast contract. A global fastMode or
400
+ // stale client may still send the OpenAI service_tier parameter after the model switches wires.
401
+ if (Object.hasOwn(next, "service_tier")) { delete next.service_tier; changed = true; }
402
+ // xAI does not document OpenAI's text.verbosity control; stale catalog clients can keep sending
403
+ // it after a metadata refresh, so fail soft at the destination boundary as well.
404
+ if (isPlainObject(next.text) && Object.hasOwn(next.text, "verbosity")) {
405
+ const text = { ...next.text };
406
+ delete text.verbosity;
407
+ next.text = text;
408
+ changed = true;
409
+ }
410
+ return changed ? next : body;
411
+ }
412
+
392
413
  function isPlainObject(v: unknown): v is Record<string, unknown> {
393
414
  return !!v && typeof v === "object" && !Array.isArray(v);
394
415
  }
395
416
 
396
- function normalizeFunctionToolSchema(tool: unknown): unknown {
417
+ function normalizeFunctionToolSchema(tool: unknown, xaiTarget: boolean): unknown | undefined {
397
418
  if (!isPlainObject(tool) || tool.type !== "function") return tool;
419
+ if (xaiTarget) {
420
+ const parameters = normalizeXaiToolParameters(isPlainObject(tool.parameters) ? tool.parameters : {});
421
+ return parameters === undefined ? undefined : { ...tool, parameters };
422
+ }
398
423
  if (isPlainObject(tool.parameters) && tool.parameters.type === "object") return tool;
399
424
  return {
400
425
  ...tool,
@@ -402,16 +427,55 @@ function normalizeFunctionToolSchema(tool: unknown): unknown {
402
427
  };
403
428
  }
404
429
 
405
- function normalizeToolSchemas(body: unknown): unknown {
406
- if (!isPlainObject(body)) return body;
430
+ function reconcileToolChoiceForOmittedTools(
431
+ body: Record<string, unknown>,
432
+ omittedFunctionNames: ReadonlySet<string>,
433
+ ): Record<string, unknown> {
434
+ if (omittedFunctionNames.size === 0) return body;
435
+ const toolChoice = body.tool_choice;
436
+ if (!isPlainObject(toolChoice)) return body;
437
+ const refuse = (name: string): never => {
438
+ throw new XaiToolSchemaCompatibilityError(
439
+ `tool_choice requires function "${name}", but its parameter schema cannot be represented for this destination; `
440
+ + "relax tool_choice or simplify the tool's parameter schema",
441
+ );
442
+ };
443
+ if (toolChoice.type === "function" && typeof toolChoice.name === "string") {
444
+ return omittedFunctionNames.has(toolChoice.name) ? refuse(toolChoice.name) : body;
445
+ }
446
+ if (toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
447
+ const omitted = toolChoice.tools.filter(tool =>
448
+ isPlainObject(tool)
449
+ && tool.type === "function"
450
+ && typeof tool.name === "string"
451
+ && omittedFunctionNames.has(tool.name));
452
+ if (omitted.length === 0) return body;
453
+ const kept = toolChoice.tools.filter(tool => !omitted.includes(tool));
454
+ if (kept.length === 0) {
455
+ const first = omitted[0];
456
+ return refuse(isPlainObject(first) && typeof first.name === "string" ? first.name : "unknown");
457
+ }
458
+ return { ...body, tool_choice: { ...toolChoice, tools: kept } };
459
+ }
460
+ return body;
461
+ }
407
462
 
463
+ function normalizeToolSchemas(body: unknown, xaiTarget: boolean): unknown {
464
+ if (!isPlainObject(body)) return body;
465
+ const omittedFunctionNames = new Set<string>();
408
466
  const normalizeTools = (tools: unknown[]): unknown[] => {
409
467
  let changed = false;
410
- const normalized = tools.map((tool) => {
411
- const fixed = normalizeFunctionToolSchema(tool);
468
+ const normalized: unknown[] = [];
469
+ for (const tool of tools) {
470
+ const fixed = normalizeFunctionToolSchema(tool, xaiTarget);
471
+ if (fixed === undefined) {
472
+ changed = true;
473
+ if (isPlainObject(tool) && typeof tool.name === "string") omittedFunctionNames.add(tool.name);
474
+ continue;
475
+ }
412
476
  if (fixed !== tool) changed = true;
413
- return fixed;
414
- });
477
+ normalized.push(fixed);
478
+ }
415
479
  return changed ? normalized : tools;
416
480
  };
417
481
 
@@ -431,7 +495,10 @@ function normalizeToolSchemas(body: unknown): unknown {
431
495
  });
432
496
  if (inputChanged) normalizedBody = { ...normalizedBody, input };
433
497
  }
434
- return normalizedBody;
498
+ if (omittedFunctionNames.size > 0) {
499
+ debugProviderDiagnostic("openai-responses", "tool-schema-omitted", { omitted: [...omittedFunctionNames] });
500
+ }
501
+ return reconcileToolChoiceForOmittedTools(normalizedBody, omittedFunctionNames);
435
502
  }
436
503
 
437
504
  const MAX_RESPONSES_CALL_ID_LENGTH = 64;
@@ -1062,7 +1129,19 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1062
1129
  if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
1063
1130
  outBody = buildRoutedCompactionBody(outBody);
1064
1131
  }
1065
- const sanitizedBody = normalizeToolSchemas(stripMuseSparkUnsupportedWebSearchFields(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody))))))), parsed.modelId));
1132
+ if (isXaiResponsesDestination(provider)) {
1133
+ outBody = lowerXaiResponsesCustomTools(outBody).body;
1134
+ }
1135
+ const sanitizedBody = stripXaiOAuthOnlyParams(
1136
+ normalizeXaiResponsesWebSearch(
1137
+ normalizeToolSchemas(
1138
+ stripMuseSparkUnsupportedWebSearchFields(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody))))))), parsed.modelId, url),
1139
+ isXaiSchemaTarget(provider),
1140
+ ),
1141
+ provider,
1142
+ ),
1143
+ provider,
1144
+ );
1066
1145
  const body = JSON.stringify(stripDisabledReasoningSummaries(
1067
1146
  normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
1068
1147
  provider,
@@ -0,0 +1,78 @@
1
+ export function isSchemaObject(value: unknown): value is Record<string, unknown> {
2
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+
5
+ function decodeJsonPointerToken(token: string): string {
6
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
7
+ }
8
+
9
+ /** Resolve a local `#/`-rooted JSON Pointer against `root`; undefined when it does not resolve. */
10
+ export function lookupLocalJsonPointer(root: unknown, ref: string): unknown {
11
+ if (ref === "#" || ref === "#/") return root;
12
+ if (!ref.startsWith("#/")) return undefined;
13
+ let current: unknown = root;
14
+ for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) {
15
+ if (!isSchemaObject(current) || !Object.hasOwn(current, token)) return undefined;
16
+ current = current[token];
17
+ }
18
+ return current;
19
+ }
20
+
21
+ function xaiLiteralValues(schema: unknown): unknown[] | undefined {
22
+ if (!isSchemaObject(schema)) return undefined;
23
+ if (Object.hasOwn(schema, "const")) return [schema.const];
24
+ if (Array.isArray(schema.enum)) return schema.enum;
25
+ return undefined;
26
+ }
27
+
28
+ function xaiJsonTypeOf(value: unknown): string {
29
+ if (value === null) return "null";
30
+ if (Array.isArray(value)) return "array";
31
+ if (typeof value === "string") return "string";
32
+ if (typeof value === "boolean") return "boolean";
33
+ if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
34
+ return "object";
35
+ }
36
+
37
+ function xaiDeclaredTypes(schema: unknown): Set<string> | undefined {
38
+ if (!isSchemaObject(schema)) return undefined;
39
+ const type = schema.type;
40
+ if (typeof type === "string") return new Set([type]);
41
+ if (Array.isArray(type) && type.every(item => typeof item === "string")) return new Set(type as string[]);
42
+ return undefined;
43
+ }
44
+
45
+ function xaiTypesOverlap(left: string, right: string): boolean {
46
+ if (left === right) return true;
47
+ return (left === "integer" && right === "number") || (left === "number" && right === "integer");
48
+ }
49
+
50
+ /** Conservative proof: return true only when no instance can satisfy both schemas. */
51
+ function xaiSchemasAreProvablyDisjoint(left: unknown, right: unknown): boolean {
52
+ const leftValues = xaiLiteralValues(left);
53
+ const rightValues = xaiLiteralValues(right);
54
+ if (leftValues && rightValues) {
55
+ const seen = new Set(rightValues.map(value => JSON.stringify(value)));
56
+ return leftValues.every(value => !seen.has(JSON.stringify(value)));
57
+ }
58
+ const leftTypes = xaiDeclaredTypes(left);
59
+ const rightTypes = xaiDeclaredTypes(right);
60
+ const literalsExcludedByTypes = (values: unknown[], types: Set<string>): boolean =>
61
+ values.every(value => ![...types].some(type => xaiTypesOverlap(xaiJsonTypeOf(value), type)));
62
+ if (leftValues && rightTypes) return literalsExcludedByTypes(leftValues, rightTypes);
63
+ if (rightValues && leftTypes) return literalsExcludedByTypes(rightValues, leftTypes);
64
+ if (leftTypes && rightTypes) {
65
+ return ![...leftTypes].some(leftType => [...rightTypes].some(rightType => xaiTypesOverlap(leftType, rightType)));
66
+ }
67
+ return false;
68
+ }
69
+
70
+ /** Every pair is provably disjoint, so `anyOf` and `oneOf` accept the same union. */
71
+ export function xaiSchemasArePairwiseDisjoint(schemas: unknown[]): boolean {
72
+ for (let i = 0; i < schemas.length; i += 1) {
73
+ for (let j = i + 1; j < schemas.length; j += 1) {
74
+ if (!xaiSchemasAreProvablyDisjoint(schemas[i], schemas[j])) return false;
75
+ }
76
+ }
77
+ return true;
78
+ }
@@ -0,0 +1,274 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+ import { isSchemaObject, lookupLocalJsonPointer, xaiSchemasArePairwiseDisjoint } from "./xai-schema-analysis";
3
+
4
+ /** A selected tool whose schema was omitted must fail locally instead of silently relaxing tool choice. */
5
+ export class XaiToolSchemaCompatibilityError extends Error {}
6
+
7
+ /** Public api.x.ai accepts root object unions; only the Grok CLI proxy rejects them. */
8
+ export function isXaiSchemaTarget(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
9
+ try {
10
+ return new URL(provider.baseUrl).hostname === "cli-chat-proxy.grok.com";
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ function stringRequiredFields(value: unknown): string[] {
17
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
18
+ }
19
+
20
+ const XAI_VARIANT_MERGE_KEYS = new Set([
21
+ "type", "properties", "required", "additionalProperties", "description", "title", "$comment", "$defs", "definitions",
22
+ ]);
23
+ const XAI_MAX_SCHEMA_DEPTH = 64;
24
+ const XAI_MAX_SCHEMA_NODES = 4_096;
25
+ const XAI_MAX_ROOT_VARIANTS = 256;
26
+
27
+ interface XaiSchemaBudget {
28
+ remainingNodes: number;
29
+ remainingVariants: number;
30
+ }
31
+
32
+ function createXaiSchemaBudget(): XaiSchemaBudget {
33
+ return { remainingNodes: XAI_MAX_SCHEMA_NODES, remainingVariants: XAI_MAX_ROOT_VARIANTS };
34
+ }
35
+
36
+ function composeXaiObjectSchemas(
37
+ inherited: Record<string, unknown>,
38
+ branch: Record<string, unknown>,
39
+ ): Record<string, unknown> {
40
+ const composed: Record<string, unknown> = { ...inherited, ...branch };
41
+ const inheritedProps = isSchemaObject(inherited.properties) ? inherited.properties : undefined;
42
+ const branchProps = isSchemaObject(branch.properties) ? branch.properties : undefined;
43
+ if (inheritedProps || branchProps) {
44
+ const properties: Record<string, unknown> = { ...(inheritedProps ?? {}) };
45
+ for (const [name, value] of Object.entries(branchProps ?? {})) {
46
+ const inheritedValue = inheritedProps?.[name];
47
+ properties[name] = inheritedValue !== undefined && JSON.stringify(inheritedValue) !== JSON.stringify(value)
48
+ ? { allOf: [inheritedValue, value] }
49
+ : value;
50
+ }
51
+ composed.properties = properties;
52
+ }
53
+ const required = [...new Set([
54
+ ...stringRequiredFields(inherited.required),
55
+ ...stringRequiredFields(branch.required),
56
+ ])];
57
+ if (required.length > 0) composed.required = required;
58
+ else delete composed.required;
59
+ return composed;
60
+ }
61
+
62
+ /** Resolve local refs under a bounded walk; ambiguous/cyclic/over-budget schemas fail closed. */
63
+ function resolveXaiSchemaRefs(
64
+ schema: unknown,
65
+ root: Record<string, unknown>,
66
+ budget: XaiSchemaBudget,
67
+ stack: Set<string> = new Set(),
68
+ depth = 0,
69
+ ): unknown | undefined {
70
+ if (!isSchemaObject(schema)) return schema;
71
+ if (depth >= XAI_MAX_SCHEMA_DEPTH || budget.remainingNodes <= 0) return undefined;
72
+ budget.remainingNodes -= 1;
73
+
74
+ if (typeof schema.$ref === "string") {
75
+ const ref = schema.$ref;
76
+ if (stack.has(ref)) return undefined;
77
+ const target = lookupLocalJsonPointer(root, ref);
78
+ if (target === undefined) return undefined;
79
+ stack.add(ref);
80
+ const resolvedTarget = resolveXaiSchemaRefs(target, root, budget, stack, depth + 1);
81
+ stack.delete(ref);
82
+ if (resolvedTarget === undefined) return undefined;
83
+ const rest: Record<string, unknown> = { ...schema };
84
+ delete rest.$ref;
85
+ if (Object.keys(rest).length === 0) return resolvedTarget;
86
+ const resolvedRest = resolveXaiSchemaRefs(rest, root, budget, stack, depth + 1);
87
+ if (resolvedRest === undefined || !isSchemaObject(resolvedTarget) || !isSchemaObject(resolvedRest)) return undefined;
88
+ return composeXaiObjectSchemas(resolvedTarget, resolvedRest);
89
+ }
90
+
91
+ const resolved: Record<string, unknown> = {};
92
+ for (const [key, value] of Object.entries(schema)) {
93
+ if ((key === "oneOf" || key === "anyOf") && Array.isArray(value)) {
94
+ const items: unknown[] = [];
95
+ for (const item of value) {
96
+ const next = resolveXaiSchemaRefs(item, root, budget, stack, depth + 1);
97
+ if (next === undefined) return undefined;
98
+ items.push(next);
99
+ }
100
+ resolved[key] = items;
101
+ continue;
102
+ }
103
+ if (key === "properties" && isSchemaObject(value)) {
104
+ const properties: Record<string, unknown> = {};
105
+ for (const [name, property] of Object.entries(value)) {
106
+ const next = resolveXaiSchemaRefs(property, root, budget, stack, depth + 1);
107
+ if (next === undefined) return undefined;
108
+ properties[name] = next;
109
+ }
110
+ resolved[key] = properties;
111
+ continue;
112
+ }
113
+ resolved[key] = value;
114
+ }
115
+ return resolved;
116
+ }
117
+
118
+ function xaiVariantIsConcreteObject(variant: Record<string, unknown>): boolean {
119
+ if (variant.type !== undefined && variant.type !== "object") return false;
120
+ return Object.keys(variant).every(key => XAI_VARIANT_MERGE_KEYS.has(key));
121
+ }
122
+
123
+ function variantProperties(variant: Record<string, unknown>): Record<string, unknown> {
124
+ return isSchemaObject(variant.properties) ? variant.properties : {};
125
+ }
126
+
127
+ /** Independent per-property unions are exact only when all variants carry the same property names and <=1 schema differs. */
128
+ function xaiPropertyMergeIsLossless(variants: Record<string, unknown>[]): boolean {
129
+ const names = new Set<string>();
130
+ const props = variants.map(variant => {
131
+ const properties = variantProperties(variant);
132
+ for (const name of Object.keys(properties)) names.add(name);
133
+ return properties;
134
+ });
135
+ let schemaConflicts = 0;
136
+ for (const name of names) {
137
+ const values = props.map(property => property[name]);
138
+ if (values.some(value => value === undefined)) return false;
139
+ if (values.some(value => JSON.stringify(value) !== JSON.stringify(values[0]))) schemaConflicts += 1;
140
+ }
141
+ return schemaConflicts <= 1;
142
+ }
143
+
144
+ function xaiRequiredSetsMatch(variants: Record<string, unknown>[]): boolean {
145
+ const serialized = variants.map(variant => [...stringRequiredFields(variant.required)].sort().join("\0"));
146
+ return serialized.every(value => value === serialized[0]);
147
+ }
148
+
149
+ function uniqueXaiSchemas(values: unknown[]): unknown[] {
150
+ const unique: unknown[] = [];
151
+ const serialized = new Set<string>();
152
+ for (const value of values) {
153
+ const key = JSON.stringify(value);
154
+ if (serialized.has(key)) continue;
155
+ serialized.add(key);
156
+ unique.push(value);
157
+ }
158
+ return unique;
159
+ }
160
+
161
+ function mergeXaiAdditionalProperties(
162
+ variants: Record<string, unknown>[],
163
+ ): { ok: true; value?: unknown } | { ok: false } {
164
+ const values = variants.map(variant => variant.additionalProperties);
165
+ const explicit = values.filter(value => value !== undefined);
166
+ if (explicit.length === 0) return { ok: true };
167
+ if (explicit.length !== values.length) return { ok: false };
168
+ const hasFalse = explicit.some(value => value === false);
169
+ const permissive = explicit.filter(value => value !== false);
170
+ if (hasFalse && permissive.length > 0) return { ok: false };
171
+ if (hasFalse) return { ok: true, value: false };
172
+ const unique = uniqueXaiSchemas(permissive);
173
+ return unique.length === 1 ? { ok: true, value: unique[0] } : { ok: false };
174
+ }
175
+
176
+ interface XaiRootExpansion {
177
+ variants: Record<string, unknown>[];
178
+ isUnion: boolean;
179
+ exclusive: boolean;
180
+ nestedUnion: boolean;
181
+ }
182
+
183
+ function expandXaiRootObjectSchemas(
184
+ schema: unknown,
185
+ budget: XaiSchemaBudget,
186
+ depth = 0,
187
+ ): XaiRootExpansion | undefined {
188
+ if (!isSchemaObject(schema) || depth >= XAI_MAX_SCHEMA_DEPTH) return undefined;
189
+ const compositionKey = ["oneOf", "anyOf"].find(key => Array.isArray(schema[key]));
190
+ if (!compositionKey) {
191
+ if (schema.type !== undefined && schema.type !== "object") return undefined;
192
+ if (budget.remainingVariants <= 0) return undefined;
193
+ budget.remainingVariants -= 1;
194
+ return { variants: [{ ...schema, type: "object" }], isUnion: false, exclusive: false, nestedUnion: false };
195
+ }
196
+
197
+ const siblings = Object.fromEntries(Object.entries(schema).filter(([key]) => key !== compositionKey));
198
+ const branches = schema[compositionKey];
199
+ if (!Array.isArray(branches)) return undefined;
200
+ const expanded: Record<string, unknown>[] = [];
201
+ let exclusive = compositionKey === "oneOf";
202
+ let nestedUnion = false;
203
+ for (const branch of branches) {
204
+ const nested = expandXaiRootObjectSchemas(branch, budget, depth + 1);
205
+ if (!nested) return undefined;
206
+ exclusive ||= nested.exclusive;
207
+ nestedUnion ||= nested.isUnion || nested.nestedUnion;
208
+ for (const variant of nested.variants) expanded.push(composeXaiObjectSchemas(siblings, variant));
209
+ }
210
+ return expanded.length > 0 ? { variants: expanded, isUnion: true, exclusive, nestedUnion } : undefined;
211
+ }
212
+
213
+ /**
214
+ * Flatten only root object unions the Grok CLI proxy rejects, and only when the rewrite is lossless.
215
+ * The bounded ref/variant walk prevents adversarial tool schemas from causing exponential work.
216
+ */
217
+ export function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown> | undefined {
218
+ if (!isSchemaObject(parameters)) return undefined;
219
+ const budget = createXaiSchemaBudget();
220
+ const resolved = resolveXaiSchemaRefs(parameters, parameters, budget);
221
+ if (!isSchemaObject(resolved)) return undefined;
222
+
223
+ const normalizedRoot = { ...resolved };
224
+ delete normalizedRoot.$schema;
225
+ const expansion = expandXaiRootObjectSchemas(normalizedRoot, budget);
226
+ if (!expansion) return undefined;
227
+ const { variants, exclusive, nestedUnion } = expansion;
228
+ if (variants.length === 1) return xaiVariantIsConcreteObject(variants[0]) ? variants[0] : undefined;
229
+ if (!variants.every(xaiVariantIsConcreteObject) || !xaiRequiredSetsMatch(variants)) return undefined;
230
+ const additionalProperties = mergeXaiAdditionalProperties(variants);
231
+ if (!additionalProperties.ok || !xaiPropertyMergeIsLossless(variants)) return undefined;
232
+
233
+ const metadata = Object.fromEntries(Object.entries(normalizedRoot)
234
+ .filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
235
+ delete metadata.properties;
236
+ delete metadata.required;
237
+ delete metadata.additionalProperties;
238
+
239
+ const propertyValues = new Map<string, unknown[]>();
240
+ for (const variant of variants) {
241
+ if (!isSchemaObject(variant.properties)) continue;
242
+ for (const [name, value] of Object.entries(variant.properties)) {
243
+ const values = propertyValues.get(name) ?? [];
244
+ values.push(value);
245
+ propertyValues.set(name, values);
246
+ }
247
+ }
248
+
249
+ const properties: Record<string, unknown> = {};
250
+ const differingNames: string[] = [];
251
+ for (const [name, values] of propertyValues) {
252
+ const unique = uniqueXaiSchemas(values);
253
+ if (unique.length === 1) {
254
+ properties[name] = unique[0];
255
+ continue;
256
+ }
257
+ if (exclusive && nestedUnion) return undefined;
258
+ differingNames.push(name);
259
+ properties[name] = exclusive && !xaiSchemasArePairwiseDisjoint(unique)
260
+ ? { oneOf: unique }
261
+ : { anyOf: unique };
262
+ }
263
+
264
+ let required = stringRequiredFields(variants[0]?.required);
265
+ if (exclusive && differingNames.length > 0) required = [...new Set([...required, ...differingNames])];
266
+
267
+ return {
268
+ ...metadata,
269
+ type: "object",
270
+ properties,
271
+ ...(required.length > 0 ? { required } : {}),
272
+ ...("value" in additionalProperties ? { additionalProperties: additionalProperties.value } : {}),
273
+ };
274
+ }