mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Parse NEAR AI alternative response format with output field
The API returns a different JSON structure with output as a Value. Added extract_text_from_output() to handle various output formats: - String: return directly - Array with text/content fields: extract and join - Object with text/content/message fields: extract Both complete() and complete_with_tools() now handle this format. Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
635aeb66a7
commit
066df79595
+117
-20
@@ -96,16 +96,15 @@ impl NearAiProvider {
|
||||
});
|
||||
}
|
||||
|
||||
// Try to parse as JSON, but the API might return plain text
|
||||
// Try to parse as our expected type
|
||||
match serde_json::from_str::<R>(&response_text) {
|
||||
Ok(parsed) => Ok(parsed),
|
||||
Err(e) => {
|
||||
tracing::debug!("Response is not expected JSON format: {}", e);
|
||||
// If it's not valid JSON for our expected type, it might be a raw string response
|
||||
// We'll handle this in the caller
|
||||
tracing::debug!("Will try alternative parsing in caller");
|
||||
Err(LlmError::InvalidResponse {
|
||||
provider: "nearai".to_string(),
|
||||
reason: format!("Unexpected response format. Raw: {}", response_text),
|
||||
reason: format!("Parse error: {}. Raw: {}", e, response_text),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -126,16 +125,28 @@ impl LlmProvider for NearAiProvider {
|
||||
tools: None,
|
||||
};
|
||||
|
||||
// Try to get structured response, fall back to raw text
|
||||
// Try to get structured response, fall back to alternative formats
|
||||
let response: NearAiResponse = match self.send_request("responses", &request).await {
|
||||
Ok(r) => r,
|
||||
Err(LlmError::InvalidResponse { reason, .. })
|
||||
if reason.starts_with("Unexpected response format. Raw: ") =>
|
||||
{
|
||||
// API returned plain text instead of JSON structure
|
||||
let raw_text = reason
|
||||
.strip_prefix("Unexpected response format. Raw: ")
|
||||
.unwrap_or("");
|
||||
Err(LlmError::InvalidResponse { reason, .. }) if reason.contains("Raw: ") => {
|
||||
// Extract the raw JSON from the error
|
||||
let raw_text = reason.split("Raw: ").nth(1).unwrap_or("");
|
||||
|
||||
// Try parsing as alternative response format
|
||||
if let Ok(alt) = serde_json::from_str::<NearAiAltResponse>(raw_text) {
|
||||
tracing::info!("NEAR AI returned alternative response format");
|
||||
let text = extract_text_from_output(&alt.output);
|
||||
let usage = alt.usage.unwrap_or(NearAiUsage {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
});
|
||||
return Ok(CompletionResponse {
|
||||
content: text,
|
||||
finish_reason: FinishReason::Stop,
|
||||
input_tokens: usage.input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it's a JSON string (quoted)
|
||||
let text = if raw_text.starts_with('"') {
|
||||
@@ -241,16 +252,28 @@ impl LlmProvider for NearAiProvider {
|
||||
tools: if tools.is_empty() { None } else { Some(tools) },
|
||||
};
|
||||
|
||||
// Try to get structured response, fall back to raw text
|
||||
// Try to get structured response, fall back to alternative formats
|
||||
let response: NearAiResponse = match self.send_request("responses", &request).await {
|
||||
Ok(r) => r,
|
||||
Err(LlmError::InvalidResponse { reason, .. })
|
||||
if reason.starts_with("Unexpected response format. Raw: ") =>
|
||||
{
|
||||
// API returned plain text instead of JSON structure
|
||||
let raw_text = reason
|
||||
.strip_prefix("Unexpected response format. Raw: ")
|
||||
.unwrap_or("");
|
||||
Err(LlmError::InvalidResponse { reason, .. }) if reason.contains("Raw: ") => {
|
||||
let raw_text = reason.split("Raw: ").nth(1).unwrap_or("");
|
||||
|
||||
// Try parsing as alternative response format
|
||||
if let Ok(alt) = serde_json::from_str::<NearAiAltResponse>(raw_text) {
|
||||
tracing::info!("NEAR AI returned alternative response format (tool request)");
|
||||
let text = extract_text_from_output(&alt.output);
|
||||
let usage = alt.usage.unwrap_or(NearAiUsage {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
});
|
||||
return Ok(ToolCompletionResponse {
|
||||
content: if text.is_empty() { None } else { Some(text) },
|
||||
tool_calls: vec![],
|
||||
finish_reason: FinishReason::Stop,
|
||||
input_tokens: usage.input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
});
|
||||
}
|
||||
|
||||
let text = if raw_text.starts_with('"') {
|
||||
serde_json::from_str::<String>(raw_text)
|
||||
@@ -382,6 +405,7 @@ struct NearAiTool {
|
||||
parameters: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Primary response format (output array style)
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NearAiResponse {
|
||||
#[allow(dead_code)]
|
||||
@@ -390,6 +414,21 @@ struct NearAiResponse {
|
||||
usage: NearAiUsage,
|
||||
}
|
||||
|
||||
/// Alternative response format (OpenAI-compatible style)
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NearAiAltResponse {
|
||||
#[allow(dead_code)]
|
||||
id: String,
|
||||
#[allow(dead_code)]
|
||||
object: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
status: Option<String>,
|
||||
/// The actual output content
|
||||
output: Option<serde_json::Value>,
|
||||
/// Usage stats
|
||||
usage: Option<NearAiUsage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NearAiOutputItem {
|
||||
#[serde(rename = "type")]
|
||||
@@ -427,6 +466,64 @@ struct NearAiErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
/// Extract text content from various output formats.
|
||||
fn extract_text_from_output(output: &Option<serde_json::Value>) -> String {
|
||||
let Some(output) = output else {
|
||||
return String::new();
|
||||
};
|
||||
|
||||
// If output is a string, return it directly
|
||||
if let Some(s) = output.as_str() {
|
||||
return s.to_string();
|
||||
}
|
||||
|
||||
// If output is an array, try to extract text from items
|
||||
if let Some(arr) = output.as_array() {
|
||||
let texts: Vec<String> = arr
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
// Check for direct text field
|
||||
if let Some(text) = item.get("text").and_then(|t| t.as_str()) {
|
||||
return Some(text.to_string());
|
||||
}
|
||||
// Check for content array with text
|
||||
if let Some(content) = item.get("content").and_then(|c| c.as_array()) {
|
||||
let content_texts: Vec<String> = content
|
||||
.iter()
|
||||
.filter_map(|c| c.get("text").and_then(|t| t.as_str()).map(String::from))
|
||||
.collect();
|
||||
if !content_texts.is_empty() {
|
||||
return Some(content_texts.join(""));
|
||||
}
|
||||
}
|
||||
// Check for content as string
|
||||
if let Some(content) = item.get("content").and_then(|c| c.as_str()) {
|
||||
return Some(content.to_string());
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
return texts.join("");
|
||||
}
|
||||
|
||||
// If output is an object, try common fields
|
||||
if let Some(obj) = output.as_object() {
|
||||
if let Some(text) = obj.get("text").and_then(|t| t.as_str()) {
|
||||
return text.to_string();
|
||||
}
|
||||
if let Some(content) = obj.get("content").and_then(|c| c.as_str()) {
|
||||
return content.to_string();
|
||||
}
|
||||
if let Some(message) = obj.get("message").and_then(|m| m.as_str()) {
|
||||
return message.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return JSON representation
|
||||
tracing::warn!("Could not extract text from output: {:?}", output);
|
||||
output.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user