fix(p3): remediate top audit findings#66
Conversation
Three concrete fixes addressing audit L5 (observability), L26 (resilience),
and L14 (error recovery hints):
1. Integrate tracing instrumentation (L5)
- Added #[tracing::instrument] to OpenAiProvider::complete()
- Added tracing::info!/error!/warn! events for routing decisions,
provider errors, and retry lifecycle
- tracing = "0.1" dep is now used, fixing dead-dependency lint
2. Enforce timeout on HTTP requests (L26)
- Client built with reqwest::ClientBuilder and 120s default timeout
- Per-request timeout applied from CompletionRequest::timeout_ms
- Reqwest timeout errors correctly mapped to LlmError::Timeout
- Added test
3. Add retry with exponential backoff (L26) and recovery hints (L14)
- LlmRouter::complete_with_retry with 3 attempts: 100ms, 200ms, 400ms
- Only retries on transient (Provider, RateLimited) errors
- LlmError::is_retryable() and LlmError::recovery_hint() methods
- Error variants enriched with structured fields instead of unit variants
Tests added:
- complete_with_retry_succeeds_on_first_try
- complete_with_retry_exhausts_after_max_attempts
- complete_with_retry_does_not_retry_non_retryable_errors
- llm_error_recovery_hints_are_distinct
- llm_error_is_retryable_classification
- tracing_events_are_emitted_on_routing_decision
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
CodeAnt AI is reviewing your PR. |
Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
Warning Review limit reached
Next review available in: 33 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
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 |
|
| let _response = http_request | ||
| .send() | ||
| .await | ||
| .map_err(|e| LlmError::Provider(e.to_string()))?; | ||
| .map_err(|e| { | ||
| if e.is_timeout() { | ||
| let timeout_ms = request.timeout_ms.unwrap_or(120000); | ||
| LlmError::Timeout { timeout_ms } | ||
| } else if e.is_connect() { | ||
| error!(provider = %self.provider_name(), error = %e, "connection refused by provider"); | ||
| LlmError::Provider(format!("connection failed: {}", e)) | ||
| } else { | ||
| error!(provider = %self.provider_name(), error = %e, "provider request failed"); | ||
| LlmError::Provider(e.to_string()) | ||
| } | ||
| })?; |
There was a problem hiding this comment.
Suggestion: Replace this placeholder-style response handling by fully consuming and mapping the provider response into the completion output rather than discarding it. [custom_rule]
Severity Level: Minor
Why it matters? 🤔
The code explicitly binds the provider response to _response and then discards it, so the implementation does not consume or map the response into CompletionResponse. This is a genuine incomplete/placeholder-style implementation in changed code.
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** crates/llm-router/src/lib.rs
**Line:** 130:144
**Comment:**
*Custom Rule: Replace this placeholder-style response handling by fully consuming and mapping the provider response into the completion output rather than discarding it.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| #[tokio::test] | ||
| async fn complete_with_retry_succeeds_on_first_try() { |
There was a problem hiding this comment.
Suggestion: Add Functional Requirement traceability (for example an FR identifier comment) to the newly added tests so they comply with the repository’s FR-to-test linkage rule. [custom_rule]
Severity Level: Minor
Why it matters? 🤔
FUNCTIONAL_REQUIREMENTS.md exists, and the newly added tests in this hunk do not reference any FR identifier in comments or names. The rule explicitly requires tests added or modified to reference a Functional Requirement when that file exists, so this is a real violation.
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** crates/llm-router/src/lib.rs
**Line:** 376:377
**Comment:**
*Custom Rule: Add Functional Requirement traceability (for example an FR identifier comment) to the newly added tests so they comply with the repository’s FR-to-test linkage rule.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| let err = LlmError::RateLimited { retry_after_ms: 2000 }; | ||
| assert!(format!("{}", err).contains("rate limited")); | ||
| assert!(format!("{}", err).contains("2000")); | ||
|
|
||
| let err = LlmError::Timeout; | ||
| assert_eq!(format!("{}", err), "timeout"); | ||
| let err = LlmError::Timeout { timeout_ms: 5000 }; | ||
| assert!(format!("{}", err).contains("timeout after 5000ms")); |
There was a problem hiding this comment.
Suggestion: Add an explicit AgilePlus spec trace (or spec creation reference) next to this new test coverage so the change is clearly linked to a tracked work item. [custom_rule]
Severity Level: Minor
Why it matters? 🤔
The added test coverage introduces new behavior checks, but the file snippet itself does not show any AgilePlus spec reference or creation path. That matches the no-code-without-spec rule, so the suggestion is verified.
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** tests/llm_router_test.rs
**Line:** 142:147
**Comment:**
*Custom Rule: Add an explicit AgilePlus spec trace (or spec creation reference) next to this new test coverage so the change is clearly linked to a tracked work item.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| let _response = http_request | ||
| .send() | ||
| .await | ||
| .map_err(|e| LlmError::Provider(e.to_string()))?; | ||
| .map_err(|e| { | ||
| if e.is_timeout() { | ||
| let timeout_ms = request.timeout_ms.unwrap_or(120000); | ||
| LlmError::Timeout { timeout_ms } | ||
| } else if e.is_connect() { | ||
| error!(provider = %self.provider_name(), error = %e, "connection refused by provider"); | ||
| LlmError::Provider(format!("connection failed: {}", e)) | ||
| } else { | ||
| error!(provider = %self.provider_name(), error = %e, "provider request failed"); | ||
| LlmError::Provider(e.to_string()) | ||
| } | ||
| })?; |
There was a problem hiding this comment.
Suggestion: The provider call only checks transport failures from .send() and never validates HTTP status codes, so 4xx/5xx API responses are treated as successful completions. This causes unauthorized, invalid-request, and server-error responses to be returned as Ok(...) instead of Err(...), which breaks error handling and retry behavior. Validate the response status (for example by converting non-2xx into errors) before returning success. [api mismatch]
Severity Level: Critical 🚨
❌ LlmRouter callers mis-handle OpenAI 4xx/5xx as success.
⚠️ Retry classification never triggers on provider HTTP failures.
❌ Recovery hints not returned for authorization or quota errors.Steps of Reproduction ✅
1. A library consumer constructs an `OpenAiProvider` and registers it on an `LlmRouter`
(provider struct and router defined in `crates/llm-router/src/lib.rs:86-105` and
`175-188`), then calls `LlmRouter::complete(&request)` to send a chat completion to
OpenAI.
2. `LlmRouter::complete` at `crates/llm-router/src/lib.rs:196-221` routes the request to
the provider and invokes `OpenAiProvider::complete(&request)` at
`crates/llm-router/src/lib.rs:108-167`.
3. Inside `OpenAiProvider::complete`, the HTTP request is built at
`crates/llm-router/src/lib.rs:113-124`, an optional per-request timeout is applied at
`126-128`, and `http_request.send().await` is executed at `130-132`, with only
transport-level errors mapped in the `map_err` closure at `133-144`.
4. When OpenAI returns a non-2xx HTTP status (e.g., 401 unauthorized or 500 server error),
`reqwest` still yields `Ok(Response)`; because the code at `130-144` does not inspect
`response.status()` or body for API errors, execution proceeds to construct a synthetic
`CompletionResponse` at `160-166`, logs `"completion succeeded"` at `152-158`, and returns
`Ok(...)` to `LlmRouter::complete`, causing callers to treat failed API responses as
successful completions.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** crates/llm-router/src/lib.rs
**Line:** 130:144
**Comment:**
*Api Mismatch: The provider call only checks transport failures from `.send()` and never validates HTTP status codes, so 4xx/5xx API responses are treated as successful completions. This causes unauthorized, invalid-request, and server-error responses to be returned as `Ok(...)` instead of `Err(...)`, which breaks error handling and retry behavior. Validate the response status (for example by converting non-2xx into errors) before returning success.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| let max_attempts: u32 = 3; | ||
| let mut last_error = None; | ||
|
|
||
| for attempt in 1..=max_attempts { | ||
| match self.complete(request).await { | ||
| Ok(response) => { | ||
| if attempt > 1 { | ||
| info!(attempt, "retry succeeded"); | ||
| } | ||
| return Ok(response); | ||
| } | ||
| Err(e) if e.is_retryable() && attempt < max_attempts => { | ||
| let delay_ms = 100u64 * 2u64.pow(attempt - 1); |
There was a problem hiding this comment.
Suggestion: The retry loop is configured for only 3 total attempts, but the backoff formula/documentation expects delays of 100/200/400ms; with the current attempt < max_attempts guard, only 100ms and 200ms delays are ever used. This is an off-by-one in retry scheduling and reduces resilience versus the stated behavior. Align attempt count and delay schedule so the intended third backoff interval is actually reachable. [off-by-one]
Severity Level: Major ⚠️
⚠️ Third configured backoff interval never executes in retries.
⚠️ Transient failures retried less conservatively than documented.
⚠️ Resilience against persistent provider errors slightly reduced.Steps of Reproduction ✅
1. The retry helper `LlmRouter::complete_with_retry` is defined at
`crates/llm-router/src/lib.rs:226-263` and documented at `224-225` as using exponential
backoff of `100ms, 200ms, 400ms` for transient errors.
2. In `complete_with_retry`, `max_attempts` is set to `3` and the loop `for attempt in
1..=max_attempts` at `233` drives up to three calls to `self.complete(request).await`, as
verified by the failing test provider in `TestProvider::complete` at
`crates/llm-router/src/lib.rs:300-307` which always returns `Err(LlmError::Provider("Mock
failure"))`.
3. Because `LlmError::Provider` is classified as retryable by `LlmError::is_retryable` at
`crates/llm-router/src/lib.rs:27-29`, errors from `TestProvider::complete` enter the
`Err(e) if e.is_retryable() && attempt < max_attempts` branch at `241`.
4. In that branch, the delay is computed as `delay_ms = 100u64 * 2u64.pow(attempt - 1)` at
`242`; with `max_attempts = 3`, this runs only for `attempt = 1` and `attempt = 2`
(yielding 100ms and 200ms), while the third attempt (`attempt = 3`) goes directly into the
`Err(e)` arm at `253-258` without a 400ms sleep, so the documented third backoff interval
is never used even when all three attempts are exhausted (e.g., the
`complete_with_retry_exhausts_after_max_attempts` test at
`crates/llm-router/src/lib.rs:394-408`).(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** crates/llm-router/src/lib.rs
**Line:** 230:242
**Comment:**
*Off By One: The retry loop is configured for only 3 total attempts, but the backoff formula/documentation expects delays of 100/200/400ms; with the current `attempt < max_attempts` guard, only 100ms and 200ms delays are ever used. This is an off-by-one in retry scheduling and reduces resilience versus the stated behavior. Align attempt count and delay schedule so the intended third backoff interval is actually reachable.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix|
CodeAnt AI finished reviewing your PR. |
|
Closing duplicate — superseded by #67 (identical title, newer) |



User description
Summary
Remediates three P3 findings from audit-run-v37 in
phenoAI:1. Integrate
tracinginstrumentation (L5 Observability)The
tracing = "0.1"dependency was declared but never used. Now integrated:#[tracing::instrument(skip(...))]onOpenAiProvider::complete()tracing::info!on successful completions and routing decisionstracing::error!on provider failurestracing::warn!on transient errors and retries2. Enforce HTTP timeout (L26 Resilience)
LlmError::TimeoutandCompletionRequest::timeout_msexisted but were never enforced:reqwest::Clientnow built withClientBuilder::timeout(120s)timeout_msapplied via.timeout(Duration::from_millis(ms))LlmError::Timeout { timeout_ms }3. Retry with exponential backoff + recovery hints (L26/L14)
LlmRouter::complete_with_retry()— 3 attempts with 100/200/400ms backoffLlmError::is_retryable()— only retries Provider/RateLimited errorsLlmError::recovery_hint()— human-readable recovery suggestion per variantRateLimited { retry_after_ms },Timeout { timeout_ms }Testing
CodeAnt-AI Description
Fix LLM routing errors, timeouts, and retry behavior
What Changed
Impact
✅ Fewer stalled AI requests✅ Clearer error messages for failed completions✅ Faster recovery from temporary provider failures💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.