[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,6 +119,60 @@ 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();
|
||||||
|
|
||||||
|
// IMPORTANT: Check recipient FIRST before channel
|
||||||
|
// The model sometimes generates tool calls with channel="analysis" + recipient="functions.*"
|
||||||
|
// instead of channel="commentary" + recipient="functions.*"
|
||||||
|
// We should trust the recipient field to determine if this is a tool call
|
||||||
|
if let Some(recipient_str) = recipient {
|
||||||
|
if recipient_str.starts_with("functions.") {
|
||||||
|
// This is a tool call, regardless of channel
|
||||||
|
let function_name = recipient_str.strip_prefix("functions.").unwrap();
|
||||||
|
|
||||||
|
// Process each content item separately
|
||||||
|
for content in &msg.content {
|
||||||
|
if let openai_harmony::chat::Content::Text(tc) = content {
|
||||||
|
let call_id = format!("call_{}", Uuid::new_v4());
|
||||||
|
let tool_call = ToolCall {
|
||||||
|
id: call_id,
|
||||||
|
tool_type: "function".to_string(),
|
||||||
|
function: FunctionCallResponse {
|
||||||
|
name: function_name.to_string(),
|
||||||
|
arguments: Some(tc.text.clone()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match commentary.as_mut() {
|
||||||
|
Some(calls) => calls.push(tool_call),
|
||||||
|
None => commentary = Some(vec![tool_call]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skip further channel processing for this message
|
||||||
|
continue;
|
||||||
|
} else if recipient_str.starts_with("python")
|
||||||
|
|| recipient_str.starts_with("browser")
|
||||||
|
|| recipient_str.starts_with("container")
|
||||||
|
{
|
||||||
|
// Built-in tools → treat as reasoning
|
||||||
|
// For Chat API, we add to analysis content
|
||||||
|
let text = Self::extract_text_from_content(&msg.content);
|
||||||
|
|
||||||
|
if !text.is_empty() {
|
||||||
|
// Append to analysis (built-in tools are reasoning)
|
||||||
|
match analysis.as_mut() {
|
||||||
|
Some(existing) => {
|
||||||
|
existing.push('\n');
|
||||||
|
existing.push_str(&text);
|
||||||
|
}
|
||||||
|
None => analysis = Some(text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skip further channel processing
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process by channel (only if not already handled by recipient)
|
||||||
match channel {
|
match channel {
|
||||||
"analysis" => {
|
"analysis" => {
|
||||||
// Process each content item
|
// Process each content item
|
||||||
@@ -130,51 +184,25 @@ impl HarmonyParserAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"commentary" => {
|
"commentary" => {
|
||||||
// Handle different recipient types
|
// If we reach here, recipient was not "functions.*" or built-in tools
|
||||||
if let Some(recipient_str) = recipient {
|
// Commentary channel should always have a recipient
|
||||||
if recipient_str.starts_with("functions.") {
|
// This is likely a model bug - log warning and treat as reasoning
|
||||||
let function_name = recipient_str.strip_prefix("functions.").unwrap();
|
tracing::warn!(
|
||||||
|
channel = "commentary",
|
||||||
|
recipient = ?recipient,
|
||||||
|
"Commentary message without valid recipient, treating as reasoning"
|
||||||
|
);
|
||||||
|
|
||||||
// Process each content item separately
|
let text = Self::extract_text_from_content(&msg.content);
|
||||||
for content in &msg.content {
|
|
||||||
if let openai_harmony::chat::Content::Text(tc) = content {
|
|
||||||
let call_id = format!("call_{}", Uuid::new_v4());
|
|
||||||
let tool_call = ToolCall {
|
|
||||||
id: call_id,
|
|
||||||
tool_type: "function".to_string(),
|
|
||||||
function: FunctionCallResponse {
|
|
||||||
name: function_name.to_string(),
|
|
||||||
arguments: Some(tc.text.clone()),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
match commentary.as_mut() {
|
if !text.is_empty() {
|
||||||
Some(calls) => calls.push(tool_call),
|
match analysis.as_mut() {
|
||||||
None => commentary = Some(vec![tool_call]),
|
Some(existing) => {
|
||||||
}
|
existing.push('\n');
|
||||||
}
|
existing.push_str(&text);
|
||||||
}
|
|
||||||
} else if recipient_str.starts_with("python")
|
|
||||||
|| recipient_str.starts_with("browser")
|
|
||||||
|| recipient_str.starts_with("container")
|
|
||||||
{
|
|
||||||
// Built-in tools → treat as reasoning
|
|
||||||
// For Chat API, we add to analysis content
|
|
||||||
let text = Self::extract_text_from_content(&msg.content);
|
|
||||||
|
|
||||||
if !text.is_empty() {
|
|
||||||
// Append to analysis (built-in tools are reasoning)
|
|
||||||
match analysis.as_mut() {
|
|
||||||
Some(existing) => {
|
|
||||||
existing.push('\n');
|
|
||||||
existing.push_str(&text);
|
|
||||||
}
|
|
||||||
None => analysis = Some(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