@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.
- package/gui/dist/assets/{index-BZGMtkmp.js → index-Ch-99jy3.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +12 -0
- package/src/adapters/kiro-calibration.ts +83 -0
- package/src/adapters/kiro-constants.ts +11 -2
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +12 -3
- package/src/adapters/kiro.ts +300 -78
- package/src/adapters/openai-chat.ts +1 -42
- package/src/adapters/openai-responses.ts +93 -14
- package/src/adapters/xai-schema-analysis.ts +78 -0
- package/src/adapters/xai-tool-schema.ts +274 -0
- package/src/adapters/xai-web-search.ts +138 -0
- package/src/bridge.ts +61 -6
- package/src/codex/catalog/effort.ts +4 -2
- package/src/codex/catalog/metadata.ts +38 -9
- package/src/codex/catalog/parsing.ts +17 -2
- package/src/codex/catalog/provider-fetch.ts +9 -3
- package/src/codex/catalog/sync.ts +8 -5
- package/src/codex/data/upstream-models.json +169 -0
- package/src/grok/inject.ts +1 -1
- package/src/lib/token-estimate.ts +42 -38
- package/src/lib/translator-budget.ts +34 -0
- package/src/oauth/index.ts +10 -4
- package/src/oauth/kiro.ts +71 -6
- package/src/oauth/store.ts +3 -1
- package/src/oauth/types.ts +4 -0
- package/src/providers/derive.ts +7 -5
- package/src/providers/opencode-go-transport.ts +18 -0
- package/src/providers/registry.ts +34 -10
- package/src/providers/xai-transport.ts +10 -0
- package/src/responses/compaction.ts +8 -1
- package/src/responses/namespace-aliases.ts +56 -0
- package/src/responses/parser.ts +12 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/snapshot-policy.ts +108 -0
- package/src/responses/state.ts +23 -10
- package/src/responses/turn-termination.ts +108 -0
- package/src/responses/xai-custom-tool-compat.ts +237 -0
- package/src/server/grok-responses-snapshot-repair.ts +338 -0
- package/src/server/index.ts +2 -1
- package/src/server/relay-eager.ts +1 -0
- package/src/server/responses/core.ts +173 -13
- package/src/server/responses-image-gen-repair.ts +2 -2
- package/src/server/sse-payload-rewrite.ts +20 -3
- package/src/types.ts +10 -1
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +7 -0
- 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
|
-
|
|
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
|
|
406
|
-
|
|
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 =
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|