fix: harden OpenAI-compatible parsing for OpenRouter responses#304
fix: harden OpenAI-compatible parsing for OpenRouter responses#304
Conversation
WalkthroughThis change replaces hardcoded "OpenAI" strings with dynamic provider labels in error messages, extends OpenAI response parsing to handle content arrays, legacy tool calls, and reasoning fallbacks, and adds robust text collection and tool argument parsing logic with improved error handling. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| } | ||
| if arguments_field.is_null() { | ||
| serde_json::json!({}) | ||
| } else { |
There was a problem hiding this comment.
arguments_field.clone() will pass through non-object JSON (arrays/numbers), which can make downstream tool-arg handling inconsistent. Consider only accepting objects and defaulting to {} otherwise.
| } else { | |
| fn parse_openai_tool_arguments(arguments_field: &serde_json::Value) -> serde_json::Value { | |
| if let Some(raw) = arguments_field.as_str() { | |
| return serde_json::from_str(raw).unwrap_or_else(|_| serde_json::json!({})); | |
| } | |
| if arguments_field.is_object() { | |
| arguments_field.clone() | |
| } else { | |
| serde_json::json!({}) | |
| } | |
| } |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/llm/model.rs (1)
1519-1554: Consider adding recursion depth limit for defensive coding.The recursive
collect_openai_text_contentfunction has no depth limit. While real API responses are unlikely to be deeply nested, malformed or adversarial responses could cause stack overflow.This is a low-risk concern since provider responses are generally well-formed, but a depth guard would add resilience.
🛡️ Optional: Add depth limit for robustness
-fn collect_openai_text_content(value: &serde_json::Value, text_parts: &mut Vec<String>) { +fn collect_openai_text_content(value: &serde_json::Value, text_parts: &mut Vec<String>) { + collect_openai_text_content_inner(value, text_parts, 0); +} + +fn collect_openai_text_content_inner(value: &serde_json::Value, text_parts: &mut Vec<String>, depth: usize) { + const MAX_DEPTH: usize = 32; + if depth > MAX_DEPTH { + return; + } match value { serde_json::Value::String(text) => { if !text.trim().is_empty() { text_parts.push(text.to_string()); } } serde_json::Value::Array(items) => { for item in items { - collect_openai_text_content(item, text_parts); + collect_openai_text_content_inner(item, text_parts, depth + 1); } } serde_json::Value::Object(map) => { // ... existing text/summary/refusal extraction ... if let Some(content) = map.get("content") { - collect_openai_text_content(content, text_parts); + collect_openai_text_content_inner(content, text_parts, depth + 1); } } _ => {} } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/llm/model.rs` around lines 1519 - 1554, The recursive function collect_openai_text_content lacks a depth guard and could overflow on maliciously deep JSON; modify collect_openai_text_content to accept a current_depth parameter (or use an internal helper) and a MAX_DEPTH constant (e.g., 64), immediately return when current_depth >= MAX_DEPTH, and increment current_depth on every recursive call (for array items and nested "content" values and object traversal) so nested recursion stops once the depth limit is reached; keep behavior identical otherwise (still collect strings from "text", "summary", "refusal", arrays, and content).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/llm/model.rs`:
- Around line 1519-1554: The recursive function collect_openai_text_content
lacks a depth guard and could overflow on maliciously deep JSON; modify
collect_openai_text_content to accept a current_depth parameter (or use an
internal helper) and a MAX_DEPTH constant (e.g., 64), immediately return when
current_depth >= MAX_DEPTH, and increment current_depth on every recursive call
(for array items and nested "content" values and object traversal) so nested
recursion stops once the depth limit is reached; keep behavior identical
otherwise (still collect strings from "text", "summary", "refusal", arrays, and
content).
Summary
message.contentpayloads.reasoning,reasoning_content, andreasoning_detailsas valid fallback text when content is otherwise empty, and support bothtool_callsand legacyfunction_calloutputs.finish_reasoncontext in empty-response errors.Example Response Shape
{ "choices": [ { "message": { "content": [{"type": "output_text", "text": "done"}], "reasoning": "working through the plan", "function_call": {"name": "reply", "arguments": "{\"content\":\"ok\"}"} }, "finish_reason": "function_call" } ] }Testing
./scripts/preflight.sh./scripts/gate-pr.sh