Skip to content
Merged
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
27 changes: 11 additions & 16 deletions src/adapters/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,14 +543,6 @@ function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
}
}

function isKimiSchemaTarget(provider: OcxProviderConfig): boolean {
try {
return new URL(provider.baseUrl).hostname === "api.kimi.com";
} catch {
return false;
}
}

// Volcengine Ark regional endpoints. Ark validates an assistant message's text field as a
// REQUIRED parameter and treats "" as absent, so a tool-call-only assistant in history 400s with
// `MissingParameter: input.content.text` (#796). Every other OpenAI-compatible provider accepts
Expand Down Expand Up @@ -589,11 +581,16 @@ function emptyAssistantContent(provider: OcxProviderConfig): string | { type: "t
}

/**
* Kimi requires function.parameters.type to be exactly "object" at the root.
* Codex tools with oneOf/anyOf schemas omit the root type, causing 400 errors.
* Add type: "object" at the root while preserving oneOf, $defs, and other schema keys.
* Providers like Kimi and DeepSeek reject function parameter schemas whose root
* `type` is missing or `null` — JSON Schema requires `"object"` at the root of
* function parameters. Add `type: "object"` at the root while preserving
* `oneOf`, `$defs`, and every other schema key.
*
* This mirrors `normalizeFunctionToolSchema` in openai-responses.ts, which
* applies the same root-only normalization unconditionally on the responses
* path. Nested schema content is intentionally left untouched.
*/
function ensureKimiRootObjectType(parameters: unknown): Record<string, unknown> {
function ensureRootObjectType(parameters: unknown): Record<string, unknown> {
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
return { type: "object", properties: {} };
}
Expand Down Expand Up @@ -644,13 +641,11 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
: parsed.context.tools;
if (tools.length === 0) return undefined;
const xaiTarget = isXaiSchemaTarget(provider);
const kimiTarget = isKimiSchemaTarget(provider);
const formatted = tools.flatMap(t => {
const parameters = xaiTarget
? normalizeXaiToolParameters(t.parameters)
: kimiTarget
? ensureKimiRootObjectType(t.parameters)
: t.parameters;
: ensureRootObjectType(t.parameters);
Comment thread
Wibias marked this conversation as resolved.

if (parameters === undefined) return [];
return [{
type: "function",
Expand Down
19 changes: 17 additions & 2 deletions tests/xai-transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe("xAI auth-mode transport selection", () => {
});
});

test("flattens nested root tool unions for xAI without changing other providers", () => {
test("flattens nested root tool unions for xAI while other providers get a root object type", () => {
const schema = {
oneOf: [
{ type: "object", properties: { mode: { type: "string", enum: ["view"] } } },
Expand All @@ -120,7 +120,22 @@ describe("xAI auth-mode transport selection", () => {
...parsed(),
context: { messages: [], tools: [{ name: "automation_update", description: "Update", parameters: schema }] },
});
expect((JSON.parse(otherRequest.body) as { tools: Array<{ function: { parameters: unknown } }> }).tools[0].function.parameters).toEqual(schema);
expect((JSON.parse(otherRequest.body) as { tools: Array<{ function: { parameters: unknown } }> }).tools[0].function.parameters).toEqual({ ...schema, type: "object" });
});

test("non-xAI providers preserve nested nullable and annotation schema content", () => {
const schema = {
type: "object",
properties: {
path: { anyOf: [{ type: "string" }, { type: "null" }] },
opts: { default: { type: "null" }, enum: [{ type: "null" }, "a"] },
},
};
const request = createOpenAIChatAdapter({ ...provider("key"), baseUrl: "https://example.test/v1" }).buildRequest({
...parsed(),
context: { messages: [], tools: [{ name: "t", description: "T", parameters: schema }] },
});
expect((JSON.parse(request.body) as { tools: Array<{ function: { parameters: unknown } }> }).tools[0].function.parameters).toEqual(schema);
});

test("omits an xAI tool whose root schema cannot be normalized safely", () => {
Expand Down
Loading