[router][grpc] Make harmony parser checks recipient first before channel (#12713)
This commit is contained in:
@@ -106,7 +106,7 @@ impl HarmonyParserAdapter {
|
|||||||
pub fn parse_messages(
|
pub fn parse_messages(
|
||||||
messages: &[openai_harmony::chat::Message],
|
messages: &[openai_harmony::chat::Message],
|
||||||
) -> (Option<String>, Option<Vec<ToolCall>>, String) {
|
) -> (Option<String>, Option<Vec<ToolCall>>, String) {
|
||||||
let mut analysis = None;
|
let mut analysis: Option<String> = None;
|
||||||
let mut commentary: Option<Vec<ToolCall>> = None;
|
let mut commentary: Option<Vec<ToolCall>> = None;
|
||||||
let mut final_text = String::new();
|
let mut final_text = String::new();
|
||||||
|
|
||||||
@@ -119,20 +119,13 @@ impl HarmonyParserAdapter {
|
|||||||
let channel = msg.channel.as_deref().unwrap_or("");
|
let channel = msg.channel.as_deref().unwrap_or("");
|
||||||
let recipient = msg.recipient.as_deref();
|
let recipient = msg.recipient.as_deref();
|
||||||
|
|
||||||
match channel {
|
// IMPORTANT: Check recipient FIRST before channel
|
||||||
"analysis" => {
|
// The model sometimes generates tool calls with channel="analysis" + recipient="functions.*"
|
||||||
// Process each content item
|
// instead of channel="commentary" + recipient="functions.*"
|
||||||
// For Chat API, we join them into a single reasoning_content
|
// We should trust the recipient field to determine if this is a tool call
|
||||||
let text = Self::extract_text_from_content(&msg.content);
|
|
||||||
|
|
||||||
if !text.is_empty() {
|
|
||||||
analysis = Some(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"commentary" => {
|
|
||||||
// Handle different recipient types
|
|
||||||
if let Some(recipient_str) = recipient {
|
if let Some(recipient_str) = recipient {
|
||||||
if recipient_str.starts_with("functions.") {
|
if recipient_str.starts_with("functions.") {
|
||||||
|
// This is a tool call, regardless of channel
|
||||||
let function_name = recipient_str.strip_prefix("functions.").unwrap();
|
let function_name = recipient_str.strip_prefix("functions.").unwrap();
|
||||||
|
|
||||||
// Process each content item separately
|
// Process each content item separately
|
||||||
@@ -154,6 +147,8 @@ impl HarmonyParserAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Skip further channel processing for this message
|
||||||
|
continue;
|
||||||
} else if recipient_str.starts_with("python")
|
} else if recipient_str.starts_with("python")
|
||||||
|| recipient_str.starts_with("browser")
|
|| recipient_str.starts_with("browser")
|
||||||
|| recipient_str.starts_with("container")
|
|| recipient_str.starts_with("container")
|
||||||
@@ -172,9 +167,42 @@ impl HarmonyParserAdapter {
|
|||||||
None => analysis = Some(text),
|
None => analysis = Some(text),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Skip further channel processing
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process by channel (only if not already handled by recipient)
|
||||||
|
match channel {
|
||||||
|
"analysis" => {
|
||||||
|
// Process each content item
|
||||||
|
// For Chat API, we join them into a single reasoning_content
|
||||||
|
let text = Self::extract_text_from_content(&msg.content);
|
||||||
|
|
||||||
|
if !text.is_empty() {
|
||||||
|
analysis = Some(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"commentary" => {
|
||||||
|
// If we reach here, recipient was not "functions.*" or built-in tools
|
||||||
|
// Commentary channel should always have a recipient
|
||||||
|
// This is likely a model bug - log warning and treat as reasoning
|
||||||
|
tracing::warn!(
|
||||||
|
channel = "commentary",
|
||||||
|
recipient = ?recipient,
|
||||||
|
"Commentary message without valid recipient, treating as reasoning"
|
||||||
|
);
|
||||||
|
|
||||||
|
let text = Self::extract_text_from_content(&msg.content);
|
||||||
|
|
||||||
|
if !text.is_empty() {
|
||||||
|
match analysis.as_mut() {
|
||||||
|
Some(existing) => {
|
||||||
|
existing.push('\n');
|
||||||
|
existing.push_str(&text);
|
||||||
|
}
|
||||||
|
None => analysis = Some(text),
|
||||||
}
|
}
|
||||||
// Unknown recipient would raise ValueError
|
|
||||||
// For now, we silently ignore (can add logging later)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"final" => {
|
"final" => {
|
||||||
@@ -215,16 +243,9 @@ impl HarmonyParserAdapter {
|
|||||||
) -> Result<HarmonyChannelOutput, String> {
|
) -> Result<HarmonyChannelOutput, String> {
|
||||||
// Feed all tokens to the parser
|
// Feed all tokens to the parser
|
||||||
for &token_id in output_ids {
|
for &token_id in output_ids {
|
||||||
self.parser.process(token_id).map_err(|e| {
|
self.parser
|
||||||
// Log the full output_ids context on error
|
.process(token_id)
|
||||||
tracing::error!(
|
.map_err(|e| format!("Failed to process token {}: {}", token_id, e))?;
|
||||||
token_id = token_id,
|
|
||||||
output_ids = ?output_ids,
|
|
||||||
error = %e,
|
|
||||||
"Harmony parser failed to process token"
|
|
||||||
);
|
|
||||||
format!("Failed to process token {}: {}", token_id, e)
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract all completed messages from the parser
|
// Extract all completed messages from the parser
|
||||||
@@ -240,7 +261,7 @@ impl HarmonyParserAdapter {
|
|||||||
let final_finish_reason = if commentary.is_some() {
|
let final_finish_reason = if commentary.is_some() {
|
||||||
"tool_calls".to_string()
|
"tool_calls".to_string()
|
||||||
} else {
|
} else {
|
||||||
finish_reason
|
finish_reason.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(HarmonyChannelOutput {
|
Ok(HarmonyChannelOutput {
|
||||||
|
|||||||
Reference in New Issue
Block a user