diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index b03561c2d..d6f6227bc 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -354,6 +354,61 @@ function isPlainObject(v: unknown): v is Record { 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; + 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; @@ -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", diff --git a/src/responses/parser.ts b/src/responses/parser.ts index 1a3d3e49c..3740b0386 100644 --- a/src/responses/parser.ts +++ b/src/responses/parser.ts @@ -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 => { + const p = isObj(raw) ? { ...raw } : {}; + if (p.type !== "object") p.type = "object"; + if (!isObj(p.properties)) p.properties = {}; + return p; + }; const pushFn = (t: Record, namespace?: string) => { const tool: OcxTool = { name: t.name as string, description: (t.description as string) ?? "", - parameters: (t.parameters ?? {}) as Record, + parameters: normalizeParameters(t.parameters), }; if (t.strict !== undefined) tool.strict = t.strict as boolean; if (namespace) tool.namespace = namespace;