@coseung2/opencodex 2.8.0-cs.15 → 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-BhXIu7c0.js → index-Ch-99jy3.js} +2 -2
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/packages/ocx-notch/README.md +3 -1
- package/src/adapters/base.ts +12 -0
- package/src/adapters/identity.ts +1 -1
- 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 +126 -9
- 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/cli/observe.ts +18 -3
- package/src/codex/app-server-processes.ts +3 -5
- package/src/codex/catalog/effort.ts +4 -2
- package/src/codex/catalog/metadata.ts +42 -9
- package/src/codex/catalog/parsing.ts +17 -2
- package/src/codex/catalog/provider-fetch.ts +9 -3
- package/src/codex/catalog/sync.ts +11 -5
- package/src/codex/data/upstream-models.json +169 -0
- package/src/grok/inject.ts +1 -1
- package/src/lib/errors.ts +18 -0
- 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 +59 -0
- package/src/providers/quota.ts +68 -60
- package/src/providers/registry.ts +41 -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/request-log-conversation.ts +8 -0
- package/src/server/request-log.ts +5 -4
- package/src/server/responses/core.ts +233 -16
- 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/src/usage/log.ts +1 -2
- 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,6 +261,41 @@ function normalizeConfiguredReasoningSummaryDelivery(
|
|
|
255
261
|
* - Drops tool_search_call/tool_search_output input items
|
|
256
262
|
* - Sets parallel_tool_calls to false
|
|
257
263
|
*/
|
|
264
|
+
function stripMuseSparkUnsupportedWebSearchFields(body: unknown, modelId: unknown, responseUrl: string): unknown {
|
|
265
|
+
if (!isPlainObject(body) || !isOpenCodeMuseResponses(modelId, responseUrl)) return body;
|
|
266
|
+
const rewrite = (tools: unknown[]) => {
|
|
267
|
+
let changed = false;
|
|
268
|
+
const next = tools.map(tool => {
|
|
269
|
+
if (!isPlainObject(tool) || tool.type !== "web_search") return tool;
|
|
270
|
+
const copy = { ...tool };
|
|
271
|
+
let toolChanged = false;
|
|
272
|
+
for (const field of ["search_content_types", "indexed_web_access"]) {
|
|
273
|
+
if (Object.hasOwn(copy, field)) { delete copy[field]; toolChanged = true; }
|
|
274
|
+
}
|
|
275
|
+
if (toolChanged) changed = true;
|
|
276
|
+
return toolChanged ? copy : tool;
|
|
277
|
+
});
|
|
278
|
+
return changed ? next : tools;
|
|
279
|
+
};
|
|
280
|
+
let changed = false;
|
|
281
|
+
const next: Record<string, unknown> = { ...body };
|
|
282
|
+
if (Array.isArray(body.tools)) {
|
|
283
|
+
const tools = rewrite(body.tools);
|
|
284
|
+
if (tools !== body.tools) { next.tools = tools; changed = true; }
|
|
285
|
+
}
|
|
286
|
+
if (Array.isArray(body.input)) {
|
|
287
|
+
const input = body.input.map(item => {
|
|
288
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
|
|
289
|
+
const tools = rewrite(item.tools);
|
|
290
|
+
if (tools === item.tools) return item;
|
|
291
|
+
changed = true;
|
|
292
|
+
return { ...item, tools };
|
|
293
|
+
});
|
|
294
|
+
if (changed) next.input = input;
|
|
295
|
+
}
|
|
296
|
+
return changed ? next : body;
|
|
297
|
+
}
|
|
298
|
+
|
|
258
299
|
function stripSparkCompatibility(body: unknown): unknown {
|
|
259
300
|
if (!isPlainObject(body)) return body;
|
|
260
301
|
const model = typeof body.model === "string" ? body.model : "";
|
|
@@ -351,12 +392,34 @@ function stripSparkCompatibility(body: unknown): unknown {
|
|
|
351
392
|
: body;
|
|
352
393
|
}
|
|
353
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
|
+
|
|
354
413
|
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
355
414
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
356
415
|
}
|
|
357
416
|
|
|
358
|
-
function normalizeFunctionToolSchema(tool: unknown): unknown {
|
|
417
|
+
function normalizeFunctionToolSchema(tool: unknown, xaiTarget: boolean): unknown | undefined {
|
|
359
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
|
+
}
|
|
360
423
|
if (isPlainObject(tool.parameters) && tool.parameters.type === "object") return tool;
|
|
361
424
|
return {
|
|
362
425
|
...tool,
|
|
@@ -364,16 +427,55 @@ function normalizeFunctionToolSchema(tool: unknown): unknown {
|
|
|
364
427
|
};
|
|
365
428
|
}
|
|
366
429
|
|
|
367
|
-
function
|
|
368
|
-
|
|
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
|
+
}
|
|
369
462
|
|
|
463
|
+
function normalizeToolSchemas(body: unknown, xaiTarget: boolean): unknown {
|
|
464
|
+
if (!isPlainObject(body)) return body;
|
|
465
|
+
const omittedFunctionNames = new Set<string>();
|
|
370
466
|
const normalizeTools = (tools: unknown[]): unknown[] => {
|
|
371
467
|
let changed = false;
|
|
372
|
-
const normalized =
|
|
373
|
-
|
|
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
|
+
}
|
|
374
476
|
if (fixed !== tool) changed = true;
|
|
375
|
-
|
|
376
|
-
}
|
|
477
|
+
normalized.push(fixed);
|
|
478
|
+
}
|
|
377
479
|
return changed ? normalized : tools;
|
|
378
480
|
};
|
|
379
481
|
|
|
@@ -393,7 +495,10 @@ function normalizeToolSchemas(body: unknown): unknown {
|
|
|
393
495
|
});
|
|
394
496
|
if (inputChanged) normalizedBody = { ...normalizedBody, input };
|
|
395
497
|
}
|
|
396
|
-
|
|
498
|
+
if (omittedFunctionNames.size > 0) {
|
|
499
|
+
debugProviderDiagnostic("openai-responses", "tool-schema-omitted", { omitted: [...omittedFunctionNames] });
|
|
500
|
+
}
|
|
501
|
+
return reconcileToolChoiceForOmittedTools(normalizedBody, omittedFunctionNames);
|
|
397
502
|
}
|
|
398
503
|
|
|
399
504
|
const MAX_RESPONSES_CALL_ID_LENGTH = 64;
|
|
@@ -1024,7 +1129,19 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1024
1129
|
if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
|
|
1025
1130
|
outBody = buildRoutedCompactionBody(outBody);
|
|
1026
1131
|
}
|
|
1027
|
-
|
|
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
|
+
);
|
|
1028
1145
|
const body = JSON.stringify(stripDisabledReasoningSummaries(
|
|
1029
1146
|
normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
|
|
1030
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
|
+
}
|