fix: sanitize tool call IDs for OpenAI compatibility
OpenAI has a strict 40-character limit for tool call IDs. Long IDs (like Gemini's 56-char thought signatures) are now deterministically truncated to 40 characters when sending history back to OpenAI-compatible providers.
This commit is contained in:
@@ -29,7 +29,14 @@ pub async fn messages_to_openai_json(messages: &[UnifiedMessage]) -> Result<Vec<
|
|||||||
"content": text_content
|
"content": text_content
|
||||||
});
|
});
|
||||||
if let Some(tool_call_id) = &m.tool_call_id {
|
if let Some(tool_call_id) = &m.tool_call_id {
|
||||||
msg["tool_call_id"] = serde_json::json!(tool_call_id);
|
// OpenAI and others have a 40-char limit for tool_call_id.
|
||||||
|
// Gemini signatures (56 chars) must be shortened for compatibility.
|
||||||
|
let id = if tool_call_id.len() > 40 {
|
||||||
|
&tool_call_id[..40]
|
||||||
|
} else {
|
||||||
|
tool_call_id
|
||||||
|
};
|
||||||
|
msg["tool_call_id"] = serde_json::json!(id);
|
||||||
}
|
}
|
||||||
if let Some(name) = &m.name {
|
if let Some(name) = &m.name {
|
||||||
msg["name"] = serde_json::json!(name);
|
msg["name"] = serde_json::json!(name);
|
||||||
@@ -60,14 +67,28 @@ pub async fn messages_to_openai_json(messages: &[UnifiedMessage]) -> Result<Vec<
|
|||||||
|
|
||||||
let mut msg = serde_json::json!({ "role": m.role });
|
let mut msg = serde_json::json!({ "role": m.role });
|
||||||
|
|
||||||
|
// Include reasoning_content if present (DeepSeek R1/reasoner requires this in history)
|
||||||
|
if let Some(reasoning) = &m.reasoning_content {
|
||||||
|
msg["reasoning_content"] = serde_json::json!(reasoning);
|
||||||
|
}
|
||||||
|
|
||||||
// For assistant messages with tool_calls, content can be null
|
// For assistant messages with tool_calls, content can be null
|
||||||
if let Some(tool_calls) = &m.tool_calls {
|
if let Some(tool_calls) = &m.tool_calls {
|
||||||
|
// Sanitize tool call IDs for OpenAI compatibility (max 40 chars)
|
||||||
|
let sanitized_calls: Vec<_> = tool_calls.iter().map(|tc| {
|
||||||
|
let mut sanitized = tc.clone();
|
||||||
|
if sanitized.id.len() > 40 {
|
||||||
|
sanitized.id = sanitized.id[..40].to_string();
|
||||||
|
}
|
||||||
|
sanitized
|
||||||
|
}).collect();
|
||||||
|
|
||||||
if parts.is_empty() {
|
if parts.is_empty() {
|
||||||
msg["content"] = serde_json::Value::Null;
|
msg["content"] = serde_json::Value::Null;
|
||||||
} else {
|
} else {
|
||||||
msg["content"] = serde_json::json!(parts);
|
msg["content"] = serde_json::json!(parts);
|
||||||
}
|
}
|
||||||
msg["tool_calls"] = serde_json::json!(tool_calls);
|
msg["tool_calls"] = serde_json::json!(sanitized_calls);
|
||||||
} else {
|
} else {
|
||||||
msg["content"] = serde_json::json!(parts);
|
msg["content"] = serde_json::json!(parts);
|
||||||
}
|
}
|
||||||
@@ -109,7 +130,13 @@ pub async fn messages_to_openai_json_text_only(
|
|||||||
"content": text_content
|
"content": text_content
|
||||||
});
|
});
|
||||||
if let Some(tool_call_id) = &m.tool_call_id {
|
if let Some(tool_call_id) = &m.tool_call_id {
|
||||||
msg["tool_call_id"] = serde_json::json!(tool_call_id);
|
// OpenAI and others have a 40-char limit for tool_call_id.
|
||||||
|
let id = if tool_call_id.len() > 40 {
|
||||||
|
&tool_call_id[..40]
|
||||||
|
} else {
|
||||||
|
tool_call_id
|
||||||
|
};
|
||||||
|
msg["tool_call_id"] = serde_json::json!(id);
|
||||||
}
|
}
|
||||||
if let Some(name) = &m.name {
|
if let Some(name) = &m.name {
|
||||||
msg["name"] = serde_json::json!(name);
|
msg["name"] = serde_json::json!(name);
|
||||||
@@ -133,14 +160,28 @@ pub async fn messages_to_openai_json_text_only(
|
|||||||
|
|
||||||
let mut msg = serde_json::json!({ "role": m.role });
|
let mut msg = serde_json::json!({ "role": m.role });
|
||||||
|
|
||||||
|
// Include reasoning_content if present (DeepSeek R1/reasoner requires this in history)
|
||||||
|
if let Some(reasoning) = &m.reasoning_content {
|
||||||
|
msg["reasoning_content"] = serde_json::json!(reasoning);
|
||||||
|
}
|
||||||
|
|
||||||
// For assistant messages with tool_calls, content can be null
|
// For assistant messages with tool_calls, content can be null
|
||||||
if let Some(tool_calls) = &m.tool_calls {
|
if let Some(tool_calls) = &m.tool_calls {
|
||||||
|
// Sanitize tool call IDs for OpenAI compatibility (max 40 chars)
|
||||||
|
let sanitized_calls: Vec<_> = tool_calls.iter().map(|tc| {
|
||||||
|
let mut sanitized = tc.clone();
|
||||||
|
if sanitized.id.len() > 40 {
|
||||||
|
sanitized.id = sanitized.id[..40].to_string();
|
||||||
|
}
|
||||||
|
sanitized
|
||||||
|
}).collect();
|
||||||
|
|
||||||
if parts.is_empty() {
|
if parts.is_empty() {
|
||||||
msg["content"] = serde_json::Value::Null;
|
msg["content"] = serde_json::Value::Null;
|
||||||
} else {
|
} else {
|
||||||
msg["content"] = serde_json::json!(parts);
|
msg["content"] = serde_json::json!(parts);
|
||||||
}
|
}
|
||||||
msg["tool_calls"] = serde_json::json!(tool_calls);
|
msg["tool_calls"] = serde_json::json!(sanitized_calls);
|
||||||
} else {
|
} else {
|
||||||
msg["content"] = serde_json::json!(parts);
|
msg["content"] = serde_json::json!(parts);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user