fix(openai): strip internal metadata from gpt-5.4 responses
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

- Add strip_internal_metadata helper to remove prefixes like 'to=multi_tool_use.parallel'
- Clean up Thai text preambles reported in the journal
- Apply metadata stripping to both synchronous and streaming response paths
- Improve visual quality of proxied model responses
This commit is contained in:
2026-03-18 15:07:17 +00:00
parent 2e4318d84b
commit 441270317c

View File

@@ -43,6 +43,7 @@ impl OpenAIProvider {
pub fn parse_tool_uses_json(text: &str) -> Vec<crate::models::ToolCall> { pub fn parse_tool_uses_json(text: &str) -> Vec<crate::models::ToolCall> {
let mut calls = Vec::new(); let mut calls = Vec::new();
if let Some(start) = text.find("{\"tool_uses\":") { if let Some(start) = text.find("{\"tool_uses\":") {
// ... (rest of method unchanged)
// Find the end of the JSON block by matching braces // Find the end of the JSON block by matching braces
let sub = &text[start..]; let sub = &text[start..];
let mut brace_count = 0; let mut brace_count = 0;
@@ -87,6 +88,27 @@ impl OpenAIProvider {
} }
calls calls
} }
/// Strips internal metadata prefixes like 'to=multi_tool_use.parallel' from model responses.
pub fn strip_internal_metadata(text: &str) -> String {
let mut result = text.to_string();
// Patterns to strip
let patterns = [
"to=multi_tool_use.parallel",
"to=functions.multi_tool_use",
"ส่งเงินบาทไทยjson", // User reported Thai text preamble
];
for p in patterns {
if let Some(start) = result.find(p) {
// Remove the pattern and any whitespace around it
result.replace_range(start..start + p.len(), "");
}
}
result.trim().to_string()
}
} }
#[async_trait] #[async_trait]
@@ -405,6 +427,8 @@ impl super::Provider for OpenAIProvider {
tool_calls.extend(embedded_calls); tool_calls.extend(embedded_calls);
} }
content_text = Self::strip_internal_metadata(&content_text);
Ok(ProviderResponse { Ok(ProviderResponse {
content: content_text, content: content_text,
reasoning_content: None, reasoning_content: None,
@@ -773,9 +797,10 @@ impl super::Provider for OpenAIProvider {
if let Some(start) = content_buffer.find("{\"tool_uses\":") { if let Some(start) = content_buffer.find("{\"tool_uses\":") {
// Yield text before the JSON block // Yield text before the JSON block
let preamble = content_buffer[..start].to_string(); let preamble = content_buffer[..start].to_string();
if !preamble.is_empty() { let stripped_preamble = Self::strip_internal_metadata(&preamble);
if !stripped_preamble.is_empty() {
yield ProviderStreamChunk { yield ProviderStreamChunk {
content: preamble, content: stripped_preamble,
reasoning_content: None, reasoning_content: None,
finish_reason: None, finish_reason: None,
tool_calls: None, tool_calls: None,
@@ -785,6 +810,7 @@ impl super::Provider for OpenAIProvider {
} }
// Yield the tool calls // Yield the tool calls
// ... (rest of tool call yielding unchanged)
let deltas: Vec<crate::models::ToolCallDelta> = embedded_calls.into_iter().enumerate().map(|(idx, tc)| { let deltas: Vec<crate::models::ToolCallDelta> = embedded_calls.into_iter().enumerate().map(|(idx, tc)| {
crate::models::ToolCallDelta { crate::models::ToolCallDelta {
index: idx as u32, index: idx as u32,
@@ -834,8 +860,10 @@ impl super::Provider for OpenAIProvider {
} else { } else {
// Standard text, yield and clear buffer // Standard text, yield and clear buffer
let content = std::mem::take(&mut content_buffer); let content = std::mem::take(&mut content_buffer);
let stripped_content = Self::strip_internal_metadata(&content);
if !stripped_content.is_empty() {
yield ProviderStreamChunk { yield ProviderStreamChunk {
content, content: stripped_content,
reasoning_content: None, reasoning_content: None,
finish_reason: None, finish_reason: None,
tool_calls: None, tool_calls: None,
@@ -844,6 +872,7 @@ impl super::Provider for OpenAIProvider {
}; };
} }
} }
}
if finish_reason.is_some() || tool_calls.is_some() { if finish_reason.is_some() || tool_calls.is_some() {
yield ProviderStreamChunk { yield ProviderStreamChunk {