fix(openai): strip internal metadata from gpt-5.4 responses
- 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:
@@ -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,14 +860,17 @@ 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);
|
||||||
yield ProviderStreamChunk {
|
let stripped_content = Self::strip_internal_metadata(&content);
|
||||||
content,
|
if !stripped_content.is_empty() {
|
||||||
reasoning_content: None,
|
yield ProviderStreamChunk {
|
||||||
finish_reason: None,
|
content: stripped_content,
|
||||||
tool_calls: None,
|
reasoning_content: None,
|
||||||
model: model.clone(),
|
finish_reason: None,
|
||||||
usage: None,
|
tool_calls: None,
|
||||||
};
|
model: model.clone(),
|
||||||
|
usage: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user