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
98 changes: 98 additions & 0 deletions src-tauri/src/proxy/providers/codex_request_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,104 @@ fn chat_request_policy_prepares_literal_production_wire_request() {
assert!(prepared.body.get("_privateProbeField").is_none());
}

#[test]
fn mimo_chat_request_policy_applies_minimal_compatibility_for_any_provider() {
let mut provider = third_party_provider("openai_chat");
provider.settings_config["config"] = json!(
r#"model = "mimo-v2.6-pro"
model_provider = "mimo-chat"
[model_providers.mimo-chat]
base_url = "https://chat.example.test/v1"
wire_api = "chat"
"#
);
provider.settings_config["modelCatalog"] = json!({
"models": [{
"model": "mimo-v2.6-pro",
"reasoning": {
"schemaVersion": 2,
"supportStatus": "confirmed_supported",
"controlKind": "boolean",
"supportedEfforts": [],
"disableAllowed": true,
"upstream": {"format": "boolean", "parameter": "thinking"},
"outputFormat": "reasoning_content"
}
}]
});
let policy =
CodexThirdPartyRequestPolicy::compile(&provider).expect("compile MiMo Chat request policy");

let prepared = policy
.prepare(
CodexRequestTransport::ChatCompletions,
json!({
"model": "mimo-v2.6-pro",
"input": [
{
"type": "reasoning",
"content": [{"type": "reasoning_text", "text": "Need to patch safely."}]
},
{
"type": "custom_tool_call",
"call_id": "call_patch",
"name": "apply_patch",
"input": "*** Begin Patch\n*** End Patch"
},
{
"type": "custom_tool_call_output",
"call_id": "call_patch",
"output": "patched"
},
{
"role": "user",
"content": [{"type": "input_text", "text": "continue"}]
}
],
"tools": [{
"type": "custom",
"name": "apply_patch",
"description": "Apply a patch to files."
}],
"tool_choice": {"type": "custom", "name": "apply_patch"},
"parallel_tool_calls": true,
"reasoning": {"effort": "high"},
"max_output_tokens": 128,
"stream": true
}),
CodexRequestOptions::default(),
)
.expect("prepare MiMo Chat request");

assert_eq!(prepared.body["model"], "mimo-v2.6-pro");
assert_eq!(prepared.body["max_completion_tokens"], 128);
assert!(prepared.body.get("max_tokens").is_none());
assert_eq!(prepared.body["thinking"], json!({"type": "enabled"}));
assert!(prepared.body.get("reasoning_effort").is_none());
assert_eq!(prepared.body["tool_choice"], "auto");
assert!(prepared.body.get("parallel_tool_calls").is_none());
assert_eq!(prepared.body["tools"][0]["function"]["name"], "apply_patch");
assert_eq!(prepared.body["tools"][0]["function"]["strict"], true);
assert_eq!(
prepared.body["tools"][0]["function"]["parameters"],
json!({
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "Raw string input for the original custom tool. Preserve formatting exactly and follow the original tool definition embedded in the description."
}
},
"required": ["input"],
"additionalProperties": false
})
);
assert_eq!(
prepared.body["messages"][0]["reasoning_content"],
"Need to patch safely."
);
}

#[test]
fn responses_request_policy_maps_effort_model_and_provider_overrides() {
let policy = CodexThirdPartyRequestPolicy::compile(&third_party_provider("openai_responses"))
Expand Down
170 changes: 168 additions & 2 deletions src-tauri/src/proxy/providers/streaming_codex_chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::{
},
transform_codex_chat::{
chat_usage_to_responses_usage, custom_tool_input_from_chat_arguments,
delegate_mimo_function_validation, normalize_mimo_raw_patch_arguments,
response_id_from_chat_id, response_message_item_id, response_tool_call_item_from_chat_name,
response_tool_call_item_id_from_chat_name, CodexToolContext,
},
Expand Down Expand Up @@ -840,11 +841,31 @@ impl ChatToResponsesState {
continue;
}

if let Some(state) = self.tools.get_mut(&key) {
if let Some(arguments) = normalize_mimo_raw_patch_arguments(
&self.model,
&state.name,
&state.arguments,
&self.tool_context,
) {
state.arguments = arguments;
}
}

let has_malformed_arguments = self
.tools
.get(&key)
.is_some_and(|state| !streamed_tool_arguments_are_complete(&state.arguments));
if has_malformed_arguments {
let delegate_validation = has_malformed_arguments
&& self.tools.get(&key).is_some_and(|state| {
delegate_mimo_function_validation(
&self.model,
&state.name,
self.finish_reason.as_deref(),
&self.tool_context,
)
});
if has_malformed_arguments && !delegate_validation {
let (call_id_empty, args_bytes) = self
.tools
.get(&key)
Expand Down Expand Up @@ -907,7 +928,11 @@ impl ChatToResponsesState {
continue;
};
let output_index = state.output_index.unwrap_or(0);
let arguments = canonicalize_tool_arguments_str(&state.arguments);
let arguments = if delegate_validation {
state.arguments.clone()
} else {
canonicalize_tool_arguments_str(&state.arguments)
};
let is_custom_tool = self.tool_context.is_custom_tool_chat_name(&state.name);
let item = response_tool_call_item_from_chat_name(
&state.item_id,
Expand Down Expand Up @@ -1995,6 +2020,147 @@ mod tests {
assert!(output.contains("\"input\":\"ls -la\""));
}

#[tokio::test]
async fn streams_large_custom_tool_arguments_losslessly() {
check_large_custom_tool_arguments(false).await;
}

#[tokio::test]
async fn mimo_mixed_invalid_function_call_keeps_stream_and_arguments() {
let invalid = "{\"session_id\":4041,\"chars\":\"\u{3}\"}";
let context = super::super::transform_codex_chat::build_codex_tool_context_from_request(
&json!({
"tools":[{"type":"function","name":"write_stdin"},{"type":"function","name":"exec_command"}]
}),
);
let chunks = [
format!(
"data: {}\n\n",
json!({
"model":"mimo-v2.6-flash", "choices":[{"delta":{"tool_calls":[
{"index":0,"id":"bad","function":{"name":"write_stdin","arguments":invalid}},
{"index":1,"id":"good","function":{"name":"exec_command","arguments":"{\"cmd\":\"echo ok\"}"}}
]},"finish_reason":"tool_calls"}]
})
),
"data: [DONE]\n\n".to_string(),
];
let output =
collect_with_context(chunks.iter().map(String::as_str).collect(), context).await;
let events = parse_sse_events(&output);
assert!(!events.iter().any(|e| e["type"] == "response.failed"));
let completed = events
.iter()
.find(|e| e["type"] == "response.completed")
.unwrap();
assert_eq!(completed["response"]["output"].as_array().unwrap().len(), 2);
assert_eq!(completed["response"]["output"][0]["arguments"], invalid);
assert_eq!(
completed["response"]["output"][1]["arguments"],
"{\"cmd\":\"echo ok\"}"
);
let delta = events
.iter()
.filter(|e| {
e["type"] == "response.function_call_arguments.delta" && e["output_index"] == 0
})
.map(|e| e["delta"].as_str().unwrap())
.collect::<String>();
let done = events
.iter()
.find(|e| {
e["type"] == "response.function_call_arguments.done" && e["output_index"] == 0
})
.unwrap();
assert_eq!(delta, invalid);
assert_eq!(done["arguments"], invalid);
}

#[tokio::test]
async fn mimo_streams_large_raw_patch_losslessly() {
check_large_custom_tool_arguments(true).await;
}

async fn check_large_custom_tool_arguments(raw: bool) {
let payload = format!(
"*** Begin Patch\n\"quoted\"\\path\n中文🚀\n{}*** End Patch",
"+long line with \\\"quotes\\\" and \\\\slashes\n".repeat(420)
);
assert!(payload.len() > 16 * 1024);
let arguments = if raw {
payload.clone()
} else {
json!({"input": payload}).to_string()
};
let mut boundaries = vec![0];
for target in [
arguments.len() / 4,
arguments.len() / 2,
arguments.len() * 3 / 4,
] {
let mut boundary = target;
while !arguments.is_char_boundary(boundary) {
boundary += 1;
}
boundaries.push(boundary);
}
boundaries.push(arguments.len());

let mut chunks = Vec::new();
for index in 0..boundaries.len() - 1 {
let fragment = &arguments[boundaries[index]..boundaries[index + 1]];
let mut call = json!({
"index": 0,
"function": {"arguments": fragment}
});
if index == 0 {
call["id"] = json!("call_large_patch");
call["type"] = json!("function");
call["function"]["name"] = json!("apply_patch");
}
let mut choice = json!({"delta": {"tool_calls": [call]}});
if index == boundaries.len() - 2 {
choice["finish_reason"] = json!("tool_calls");
}
chunks.push(format!(
"data: {}\n\n",
json!({
"id": "chatcmpl_large_custom",
"model": "mimo-v2.6-pro",
"choices": [choice]
})
));
}
chunks.push("data: [DONE]\n\n".to_string());

let context =
super::super::transform_codex_chat::build_codex_tool_context_from_request(&json!({
"model": "mimo-v2.6-pro",
"tools": [{"type": "custom", "name": "apply_patch"}]
}));
let output =
collect_with_context(chunks.iter().map(String::as_str).collect(), context).await;
let events = parse_sse_events(&output);
let completed = events
.iter()
.find(|event| event["type"] == "response.completed")
.expect("large custom tool call should complete");
let item = &completed["response"]["output"][0];

assert_eq!(item["type"], "custom_tool_call");
assert_eq!(item["name"], "apply_patch");
assert_eq!(item["input"], payload);
assert!(events
.iter()
.any(|event| event["type"] == "response.custom_tool_call_input.delta"));
assert!(events
.iter()
.any(|event| event["type"] == "response.custom_tool_call_input.done"));
assert!(!events
.iter()
.any(|event| event["type"] == "response.failed"));
}

#[tokio::test]
async fn canonicalizes_streamed_tool_call_arguments_on_done_events() {
let output = collect(vec![
Expand Down
9 changes: 9 additions & 0 deletions src-tauri/src/proxy/providers/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ pub fn is_openai_o_series(model: &str) -> bool {
/// of the legacy `max_tokens` Chat Completions parameter.
pub fn requires_max_completion_tokens(model: &str) -> bool {
let normalized = model.to_lowercase();
let model_id = normalized.rsplit('/').next().unwrap_or(&normalized);
is_openai_o_series(&normalized)
|| normalized
.strip_prefix("gpt-")
.and_then(|rest| rest.chars().next())
.is_some_and(|c| c.is_ascii_digit() && c >= '5')
|| matches!(model_id, "mimo-v2.6-pro" | "mimo-v2.6-flash")
}

/// Detect Responses-compatible models that support reasoning effort.
Expand Down Expand Up @@ -1943,6 +1945,13 @@ mod tests {
}
}

#[test]
fn test_mimo_v26_uses_max_completion_tokens() {
assert!(requires_max_completion_tokens("mimo-v2.6-flash"));
assert!(requires_max_completion_tokens("xiaomi/mimo-v2.6-pro"));
assert!(!requires_max_completion_tokens("mimo-v2.5-pro"));
}

#[test]
fn test_anthropic_to_openai_non_o_series_keeps_max_tokens() {
let input = json!({
Expand Down
Loading
Loading