Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,61 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
return !!v && typeof v === "object" && !Array.isArray(v);
}

/**
* Ensure every function tool exposes a valid object JSON Schema at its
* `parameters` root. Strict validators (e.g. DeepSeek) reject schemas whose
* root `type` is `null`, missing, or non-`"object"` with HTTP 400.
*
* Codex tools such as `codex_app__automation_update` arrive without a
* `parameters` field, which would otherwise serialize to a schema lacking the
* root `type`. Walks both top-level `tools` and Responses Lite
* `additional_tools[].tools`. Namespace grouping is flattened by earlier strip
* steps, so any function tool seen here is already top-level.
*/
function normalizeToolSchemas(body: unknown): unknown {
if (!isPlainObject(body)) return body;

const fixFunctionParams = (tool: unknown): unknown => {
if (!isPlainObject(tool) || tool.type !== "function") return tool;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize namespace-contained function schemas too

When a Responses passthrough request carries MCP tools grouped as {type: "namespace", tools: [{type: "function", parameters: null}]} and the target is not the Spark path that flattens namespaces first, this guard returns the namespace unchanged, so the inner function still reaches strict Responses gateways with the same missing/null root schema that this patch is meant to prevent. Please recurse into namespace tools for both top-level tools and additional_tools containers, or flatten before normalizing.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

const params = isPlainObject(tool.parameters) ? { ...tool.parameters } : {};
let changed = false;
if (params.type !== "object") { params.type = "object"; changed = true; }
if (!isPlainObject(params.properties)) { params.properties = {}; changed = true; }
return changed ? { ...tool, parameters: params } : tool;
};

let changed = false;

// Top-level tools array.
if (Array.isArray(body.tools)) {
const tools = body.tools.map((t) => {
const fixed = fixFunctionParams(t);
if (fixed !== t) changed = true;
return fixed;
});
if (changed) body = { ...body, tools };
}

// Responses Lite `additional_tools` items embed their own tools arrays.
if (Array.isArray(body.input)) {
let inputChanged = false;
const input = body.input.map((item) => {
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
let innerChanged = false;
const innerTools = item.tools.map((t) => {
const fixed = fixFunctionParams(t);
if (fixed !== t) innerChanged = true;
return fixed;
});
if (innerChanged) { inputChanged = true; return { ...item, tools: innerTools }; }
return item;
});
if (inputChanged) { changed = true; body = { ...body, input }; }
}

return body;
}

const MAX_RESPONSES_CALL_ID_LENGTH = 64;
const REPAIRED_CALL_ID_PREFIX = "call_ocx_";
const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length;
Expand Down Expand Up @@ -938,7 +993,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
outBody = buildRoutedCompactionBody(outBody);
}
const sanitizedBody = stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody)))))));
const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody))))))));
return {
url,
method: "POST",
Expand Down
15 changes: 14 additions & 1 deletion src/responses/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,24 @@ function allowedToolName(tool: unknown): string | undefined {
function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
if (!tools) return undefined;
const out: OcxTool[] = [];
// Some tool definitions (e.g. Codex `codex_app__automation_update`) arrive
// without a `parameters` field or with `parameters: null`. When serialized,
// that produces a JSON Schema whose root `type` is `null` (or missing).
// Strict validators reject such schemas with HTTP 400
// ("schema must be a JSON Schema of 'type: \"object\"', got 'type: null'").
// Coerce every function tool's `parameters` to a valid object-schema root
// so all downstream adapters pass through cleanly.
const normalizeParameters = (raw: unknown): Record<string, unknown> => {
const p = isObj(raw) ? { ...raw } : {};
if (p.type !== "object") p.type = "object";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not mask schemas xAI is supposed to reject

For routed xAI requests where the client supplies an unsafe function schema such as {type: "string"}, this parser-level coercion rewrites it to an empty object schema before normalizeXaiToolParameters() can reject it, bypassing the existing safeguard that omits unnormalizable tools. That makes the model see and call a tool with an argument contract the client did not declare; limit this normalization to missing/null roots or let provider-specific sanitizers handle non-object schemas.

Useful? React with 👍 / 👎.

if (!isObj(p.properties)) p.properties = {};
return p;
};
const pushFn = (t: Record<string, unknown>, namespace?: string) => {
const tool: OcxTool = {
name: t.name as string,
description: (t.description as string) ?? "",
parameters: (t.parameters ?? {}) as Record<string, unknown>,
parameters: normalizeParameters(t.parameters),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add focused tests for schema normalization

This changes the request/tool schema behavior in src/ but the commit does not add any focused regression coverage, so cases like missing/null parameters on top-level tools and Responses Lite additional_tools can regress without a failing test. Please add targeted coverage near the existing responses parser/passthrough tests before merging.

AGENTS.md reference: AGENTS.md:L149-L151

Useful? React with 👍 / 👎.

};
if (t.strict !== undefined) tool.strict = t.strict as boolean;
if (namespace) tool.namespace = namespace;
Expand Down
Loading