Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
22776fe
fix(tui): pass xAI root unions of object branches through
asto18089 Nov 4, 2026
7552ef0
fix(tui): keep xAI root-union pass-through to the documented shape
asto18089 Nov 5, 2026
63b5944
docs(tui): name the empty union in the xai guard's flatten enumeration
asto18089 Nov 5, 2026
3a33783
test(tui): pin xAI pass-through edges and sharpen guard comments
asto18089 Nov 5, 2026
f605a12
fix(tui): flatten unions the pruner empties
asto18089 Nov 6, 2026
b6bf687
fix(tui): flatten oneOf dupes the registry pre-prunes
asto18089 Nov 6, 2026
f773e57
fix(tui): base xai union guard on normalized shape
asto18089 Nov 6, 2026
766a6b8
fix(tui): compare unions sans annotation keywords
asto18089 Nov 6, 2026
313fede
fix(tui): judge xai unions by semantic usability
asto18089 Nov 6, 2026
d13b51b
fix(tui): treat minProperties-floored union branches as non-discrimin…
asto18089 Nov 7, 2026
e6423af
fix(tui): flatten required-free unions regardless of minProperties fl…
asto18089 Nov 7, 2026
7d1d255
fix(tui): flatten unions sharing an equally-constrained required group
asto18089 Nov 7, 2026
8220bf4
fix(tui): treat root-implied shared entries as equivalent
asto18089 Nov 7, 2026
dbf7573
fix(tui): admit only pairwise-discriminable xAI root unions
asto18089 Nov 9, 2026
1a7aa91
fix(tui): sound xAI union admission pins and witness
asto18089 Nov 11, 2026
4d2aa22
fix(tui): intersect const with enum before accepting an xAI union pin
asto18089 Nov 12, 2026
b2307ce
fix(tools): judge xAI union admission on joint constraints and full b…
asto18089 Nov 13, 2026
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
96 changes: 94 additions & 2 deletions crates/tui/src/client/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1185,8 +1185,11 @@ pub(crate) fn build_chat_wire_body(
if matches!(provider, crate::config::ApiProvider::Moonshot) {
omitted_tool_names = sanitize_moonshot_chat_tools(&mut chat_tools);
}
// xAI rejects a parameters root that is not a plain object schema
// (e.g. apply_patch's root `oneOf` required-groups) with a 400.
// xAI rejects a parameters root that is neither an object schema nor
// a root-level union of object branches (e.g. apply_patch's root
// `oneOf` required-groups) with a 400 naming the tool; a documented
// union of object schemas passes through with its branch structure
// preserved (normalization still applies inside the branches).
if matches!(provider, crate::config::ApiProvider::Xai) {
for t in &mut chat_tools {
let Some(function) = t
Expand Down Expand Up @@ -8339,6 +8342,95 @@ mod image_block_wire_tests {
"a non-empty description keeps the appended-note form"
);
}

/// A root `oneOf` whose every branch declares `type: "object"` is a
/// documented-good xAI parameters root (2026-10-10 function-calling
/// doc): the xAI lane must keep the union — variants and all — instead
/// of folding it into one merged object.
#[test]
fn xai_chat_keeps_root_union_of_object_branches() {
let union_schema = serde_json::json!({
"oneOf": [
{
"type": "object",
"properties": {
"kind": {"const": "email"},
"address": {"type": "string"}
},
"required": ["kind", "address"]
},
{
"type": "object",
"properties": {
"kind": {"const": "sms"},
"phone": {"type": "string"}
},
"required": ["kind", "phone"]
}
]
});
let tool = Tool {
tool_type: None,
name: "notify".to_string(),
description: "Send a notification.".to_string(),
input_schema: union_schema,
allowed_callers: None,
defer_loading: None,
input_examples: None,
strict: None,
cache_control: None,
};
let request = MessageRequest {
model: "grok-4".to_string(),
messages: vec![Message {
role: Role::User,
content: vec![ContentBlock::Text {
text: "notify someone".to_string(),
cache_control: None,
}],
}],
max_tokens: 128,
system: None,
tools: Some(vec![tool]),
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort: None,
stream: None,
temperature: None,
top_p: None,
};

let body = build_chat_wire_body(
&request,
ApiProvider::Xai,
crate::config::DEFAULT_XAI_BASE_URL,
false,
)
.expect("xAI wire body");

let parameters = &body.body["tools"][0]["function"]["parameters"];
let branches = parameters["oneOf"]
.as_array()
.expect("root oneOf must survive");
assert_eq!(branches.len(), 2, "both object variants must survive");
assert_eq!(
branches[0]["required"],
serde_json::json!(["kind", "address"])
);
assert_eq!(
branches[1]["required"],
serde_json::json!(["kind", "phone"])
);
assert!(
parameters.get("type").is_none(),
"the root must not gain a forced object type: {parameters}"
);
assert!(
parameters.get("properties").is_none(),
"branch properties must not be merged into the root: {parameters}"
);
}
}

#[cfg(test)]
Expand Down
Loading
Loading