Skip to content
Open
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
33 changes: 31 additions & 2 deletions packages/kosong/src/providers/kimi-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ const NUMERIC_STRUCTURE_KEYS = new Set([
* a provider-compatibility normalizer, not a complete JSON Schema compiler:
* it resolves local refs, preserves combinator nodes, infers obvious
* scalar/object/array types, and falls back to `string` only for nested
* typeless property schemas. The root schema object is treated as a container
* and is not itself normalized.
* typeless property schemas. It also rewrites nodes that declare `type`
* alongside a combinator (`anyOf` / `oneOf` / `allOf`), which Moonshot
* rejects, by moving the parent `type` into each combinator item.
*/
export function normalizeKimiToolSchema(schema: Record<string, unknown>): Record<string, unknown> {
return ensureKimiPropertyTypes(derefJsonSchema(schema));
Expand All @@ -128,10 +129,36 @@ function ensureKimiPropertyTypes(schema: Record<string, unknown>): Record<string
if (!isRecord(normalized)) {
throw new Error('JSON Schema root must normalize to an object.');
}
fixCombinatorParentType(normalized);
recurseSchema(normalized);
return normalized;
}

/**
* Moonshot's tool validator rejects schemas that declare `type` on the same
* node as a combinator (`anyOf` / `oneOf` / `allOf`): the type must live on
* each combinator item instead. Distribute the parent `type` into items that
* lack one and drop it from the parent. Since the parent `type` constrained
* every variant anyway, copying it into each item preserves the semantics.
*/
function fixCombinatorParentType(node: unknown): void {
if (!isRecord(node) || !hasOwn(node, 'type')) {
return;
}
for (const key of ['anyOf', 'oneOf', 'allOf']) {
const items = node[key];
if (!Array.isArray(items) || items.length === 0) {
continue;
}
for (const item of items) {
if (isRecord(item) && !hasOwn(item, 'type')) {
item['type'] = node['type'];
Comment on lines +154 to +155

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 Retain the parent constraint on explicitly typed branches

When a combinator item already has a conflicting type, leaving it untouched while deleting the parent type changes the schema's accepted values. For example, {type:'object', anyOf:[{type:'string'}, {required:['a']}]} originally rejects strings because the parent requires an object, but the normalized schema accepts them through the first branch; {type:'object', allOf:[{type:'string'}]} similarly changes from unsatisfiable to a string schema. The rewrite must preserve the parent constraint for explicitly typed items as well, or reject conflicting schemas rather than broadening them.

Useful? React with 👍 / 👎.

}
}
delete node['type'];

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 Preserve the parent type across every combinator

When a schema contains more than one combinator, the first iteration deletes node.type, so subsequent combinators receive undefined; JSON serialization then omits those item types. For example, {type:'object', anyOf:[{required:['a']}], oneOf:[{required:['b']}]} leaves the oneOf item typeless, so Moonshot can still reject the normalized schema. Capture the parent type before the loop and delete it only after all combinators have been processed.

Useful? React with 👍 / 👎.

}
}

function hasUnresolvedDefinitionRef(node: unknown, bucketKey: string): boolean {
if (Array.isArray(node)) {
return node.some((child) => hasUnresolvedDefinitionRef(child, bucketKey));
Expand Down Expand Up @@ -300,6 +327,8 @@ function normalizeProperty(node: unknown): void {
return;
}

fixCombinatorParentType(node);

if (!hasOwn(node, 'type') && !hasAnyKey(node, TYPE_COMPLETION_SKIP_KEYS)) {
const enumValues = node['enum'];
if (Array.isArray(enumValues) && enumValues.length > 0) {
Expand Down