Skip to content

fix: improve LLM API error messages for better debugging - #200

Merged
paultranvan merged 2 commits into
devfrom
fix/generic-400-errors
Jan 15, 2026
Merged

fix: improve LLM API error messages for better debugging#200
paultranvan merged 2 commits into
devfrom
fix/generic-400-errors

Conversation

@paultranvan

@paultranvan paultranvan commented Jan 9, 2026

Copy link
Copy Markdown
Collaborator

Extract error details from LLM API responses instead of showing generic "Invalid JSON in API response" for all failures. Now displays the actual error response from the LLM provider.

Fixes #197

ℹ️ An additional integration test could be added after #194 is merged

Summary by CodeRabbit

  • New Features

    • Configurable preferred URL scheme for generated links.
    • Added a lightweight mock service and CI workflow to run API integration tests.
    • Runtime settings to control contextualization timeouts and concurrency.
  • Bug Fixes

    • Improved LLM/API streaming and non-streaming error handling with clearer user-facing error payloads.
    • Better handling of external resource errors to avoid noisy failures.
  • Documentation

    • New usage guide for a Claude-compatible backend and env var docs.
  • Tests

    • Extensive new API integration test suite and fixtures.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@paultranvan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 51 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between e36e196 and d6f67ae.

📒 Files selected for processing (2)
  • openrag/components/llm.py
  • openrag/routers/openai.py
📝 Walkthrough

Walkthrough

Adds targeted error handling and propagation for LLM HTTP/JSON failures, fortifies streaming SSE error emission, introduces ray actor timeout utilities, expands chunking/contextualization controls, adds external-resource error detection, CI API test infra and mock VLLM, and multiple test suites and config additions.

Changes

Cohort / File(s) Summary
LLM component
openrag/components/llm.py
Convert httpx.HTTPStatusError and json.JSONDecodeError into ValueError with status/body in sync and streaming paths; pre-check streaming response status and preserve exception propagation.
OpenAI router streaming
openrag/routers/openai.py
Wrap streaming loops to catch JSON decode/general exceptions, log, and emit structured SSE error payloads plus DONE; minor signature punctuation change.
Ray timeout & helpers
openrag/components/ray_utils.py, openrag/components/indexer/utils/files.py, openrag/components/indexer/loaders/pdf_loaders/marker.py
Add call_ray_actor_with_timeout util; replace direct Ray waits with timeout-wrapped calls and label tasks; update marker pool/loader to use timeout wrapper and add MarkerWorker destructor and reordered pool shutdown.
Chunking / contextualization
openrag/components/indexer/chunker/chunker.py
Add contextualization timeouts and concurrency config, filename plumbing through contextualization, CHUNK_FORMAT, timeout-aware generation, and safer empty-chunk handling.
External resource handling
openrag/components/indexer/loaders/base.py, openrag/utils/external_resource_errors.py, openrag/utils/test_external_resource_errors.py
New utility to detect external HTTP/resource errors; treat external image fetch failures as warnings (not exceptions) and add unit tests for detection logic.
URL building & router changes
openrag/routers/indexer.py, openrag/routers/utils.py
Add build_url() and PREFERRED_URL_SCHEME config; replace direct URL generation with builder; change model-availability iteration order and exception handling to re-raise HTTPExceptions and return HTTP 500 on generic errors.
Concurrency / semaphore tweaks
openrag/components/utils.py
Add small sleeps in cleanup loops to avoid busy-waiting; cap DistributedSemaphoreActor max_restarts from -1 to 5.
Constants / placeholders
openrag/consts.py
Add IMAGE_PLACEHOLDER constant.
Serializer timeout config
.hydra_config/*, openrag/components/indexer/utils/files.py
Add CONTEXTUALIZATION_TIMEOUT & MAX_CONCURRENT_CONTEXTUALIZATION and expose ray.indexer.serialize_timeout usage for SERIALIZE_TIMEOUT (3600 default).
OpenRAG test infra & mocks
.github/workflows/api_tests.yml, .github/workflows/api_tests/*, docker-compose.yaml, Dockerfile.mock-vllm, .github/workflows/api_tests/mock_vllm.py, tests/api_tests/..., pytest.ini, tests/api_tests/conftest.py
Add CI workflow, Docker-compose for tests, mock VLLM server image and server, extensive API tests and fixtures, pytest markers, and sample fixture data.
Docs & examples
.env.example, docs/content/docs/documentation/env_vars.md, CLAUDE.md
Add PREFERRED_URL_SCHEME to examples/docs and introduce CLAUDE.md documentation.
Misc / housekeeping
.gitignore, entrypoint.sh
Minor .gitignore adjustments and newline fix in entrypoint.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Router
    participant LLMComponent
    participant ExternalAPI
    participant RayActor

    Client->>Router: POST /v1/chat.completions (stream)
    Router->>LLMComponent: forward request (streaming)
    LLMComponent->>ExternalAPI: open streaming HTTP connection
    alt ExternalAPI returns status >= 400 or raises HTTPStatusError
        ExternalAPI-->>LLMComponent: error response
        LLMComponent->>Router: raise/convert to ValueError
        Router->>Client: SSE error payload
        Router->>Client: SSE DONE
    else ExternalAPI streams chunks
        ExternalAPI-->>LLMComponent: data chunks (JSON)
        LLMComponent->>Router: parsed chunks
        Router-->>Client: SSE stream chunks
        Router->>Client: SSE DONE
    end

    Note over Router,RayActor: Separate non-stream flow using Ray tasks
    Client->>Router: request triggering Ray actor work
    Router->>RayActor: submit task (ObjectRef)
    RayActor-->>Router: task in progress / completed
    alt task hangs / timeout
        Router->>RayActor: call_ray_actor_with_timeout cancels task
        Router->>Client: error/status reflecting timeout
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • Ahmath-Gadji
  • dodekapod

Poem

🐇 I hopped through streams at break of day,

Sniffed out bad bytes and chased them away,
Wrapped timeouts gentle, labeled every task,
Sent DONE when sorted — no need to ask,
A cozy burrow, tests ready to play.

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Changes include error handling improvements aligned with #197, plus additional infrastructure enhancements (chunker contextualization, Ray utilities, external resource error detection, configuration updates, and comprehensive API tests) that appear to be supporting features for robustness but are not explicitly required by the issue. Clarify whether additional changes (chunker timeouts, Ray task timeouts, external resource error detection, and API test infrastructure) are part of the scope or separate features that should be addressed separately.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix: improve LLM API error messages for better debugging' directly addresses the main objective: enhancing error handling to provide more informative error output instead of generic messages.
Linked Issues check ✅ Passed Code changes successfully address #197 by extracting actual error details from LLM API responses and surfacing them to clients instead of showing generic '400 Bad Request' messages across completions, chat/completions non-streaming, and streaming paths.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
openrag/components/llm.py (3)

39-45: Add exception chaining to preserve traceback context.

When re-raising exceptions, use raise ... from e to maintain the exception chain. This helps with debugging by preserving the original traceback.

♻️ Proposed fix
             except httpx.HTTPStatusError as e:
                 error_detail = e.response.text
                 raise ValueError(
                     f"LLM API error ({e.response.status_code}): {error_detail}"
-                )
+                ) from e
             except json.JSONDecodeError as e:
-                raise ValueError(f"Invalid JSON in API response: {str(e)}")
+                raise ValueError(f"Invalid JSON in API response: {e}") from e

71-75: Use logger.exception to capture full stack trace.

logger.error with str(e) loses the stack trace. Use logger.exception for automatic traceback inclusion, which aids debugging.

♻️ Proposed fix
                 except ValueError:
                     raise
                 except Exception as e:
-                    logger.error(f"Error while streaming chat completion: {str(e)}")
+                    logger.exception("Error while streaming chat completion")
                     raise

87-93: Add exception chaining here as well for consistency.

Same issue as in the completions method—preserve the exception chain for better debugging.

♻️ Proposed fix
                 except httpx.HTTPStatusError as e:
                     error_detail = e.response.text
                     raise ValueError(
                         f"LLM API error ({e.response.status_code}): {error_detail}"
-                    )
+                    ) from e
                 except json.JSONDecodeError as e:
-                    raise ValueError(f"Invalid JSON in API response: {str(e)}")
+                    raise ValueError(f"Invalid JSON in API response: {e}") from e
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce7a280 and d79f5ac.

📒 Files selected for processing (1)
  • openrag/components/llm.py
🧰 Additional context used
🪛 Ruff (0.14.10)
openrag/components/llm.py

41-43: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


41-43: Avoid specifying long messages outside the exception class

(TRY003)


45-45: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


45-45: Avoid specifying long messages outside the exception class

(TRY003)


45-45: Use explicit conversion flag

Replace with conversion flag

(RUF010)


66-68: Abstract raise to an inner function

(TRY301)


66-68: Avoid specifying long messages outside the exception class

(TRY003)


74-74: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


74-74: Use explicit conversion flag

Replace with conversion flag

(RUF010)


89-91: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


89-91: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: index-backup-restore
🔇 Additional comments (2)
openrag/components/llm.py (2)

63-68: Good approach for streaming error handling.

Reading the response body with await response.aread() before accessing response.text is the correct pattern for streaming responses. This properly surfaces the LLM provider's error details as intended by the PR objective.


1-2: The json.JSONDecodeError exception handling is correct. According to httpx documentation, Response.json() raises json.JSONDecodeError directly when encountering invalid JSON, not a wrapped httpx.DecodingError. The import statement and exception handlers at lines 44-45 and 92-93 are properly implemented.

However, Response.json() can also raise UnicodeDecodeError if the response bytes cannot be decoded to text. This is not currently handled and should be caught alongside JSONDecodeError.

@Ahmath-Gadji

Ahmath-Gadji commented Jan 13, 2026

Copy link
Copy Markdown
Collaborator

I’ve tested this and it works in the following cases:

  • For /completions, errors are handled properly.
  • For /chat/completions, errors are handled properly in the non-streaming case.
  • For /chat/completions in streaming mode, however, when an error occurs it is not shown to the client (at least it appears only in the logs).

It turns out this is a classic streaming response issue. Once a streaming response starts sending data to the client, the endpoint first returns a StreamingResponse with status 200 OK. If an exception occurs during streaming, we raise a 500, but it’s too late to propagate it to the client. The crash is only visible server-side, leaving the client unaware of the issue. See me added commit for that

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
openrag/components/llm.py (4)

39-45: Preserve exception chain with raise ... from e.

The original exception context is lost when re-raising. Use from e to preserve the traceback for debugging.

Suggested fix
             except httpx.HTTPStatusError as e:
                 error_detail = e.response.text
                 raise ValueError(
                     f"LLM API error ({e.response.status_code}): {error_detail}"
-                )
+                ) from e
             except json.JSONDecodeError as e:
-                raise ValueError(f"Invalid JSON in API response: {str(e)}")
+                raise ValueError(f"Invalid JSON in API response: {e}") from e

73-75: Consider using logger.exception for automatic traceback.

logger.exception automatically includes the stack trace, which is more useful for debugging than logger.error with just the message.

Suggested fix
                 except Exception as e:
-                    logger.error(f"Error while streaming chat completion: {str(e)}")
+                    logger.exception("Error while streaming chat completion")
                     raise

87-93: Same issue: preserve exception chain with raise ... from e.

For consistency with the suggested fix in the completions method, apply the same pattern here.

Suggested fix
                 except httpx.HTTPStatusError as e:
                     error_detail = e.response.text
                     raise ValueError(
                         f"LLM API error ({e.response.status_code}): {error_detail}"
-                    )
+                    ) from e
                 except json.JSONDecodeError as e:
-                    raise ValueError(f"Invalid JSON in API response: {str(e)}")
+                    raise ValueError(f"Invalid JSON in API response: {e}") from e

71-72: Consider adding a comment explaining the intent.

The except ValueError: raise pattern prevents the subsequent except Exception from swallowing detailed error messages. A brief comment would clarify this intent for future maintainers.

-                except ValueError:
-                    raise
+                except ValueError:
+                    # Re-raise to propagate detailed error messages to the caller
+                    raise
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d79f5ac and 736f17e.

📒 Files selected for processing (2)
  • openrag/components/llm.py
  • openrag/routers/openai.py
🧰 Additional context used
🧬 Code graph analysis (1)
openrag/routers/openai.py (1)
openrag/models/openai.py (2)
  • OpenAIChatCompletionRequest (14-30)
  • OpenAICompletionRequest (75-92)
🪛 Ruff (0.14.10)
openrag/routers/openai.py

217-217: Do not catch blind exception: Exception

(BLE001)


221-221: Use explicit conversion flag

Replace with conversion flag

(RUF010)

openrag/components/llm.py

41-43: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


41-43: Avoid specifying long messages outside the exception class

(TRY003)


45-45: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


45-45: Avoid specifying long messages outside the exception class

(TRY003)


45-45: Use explicit conversion flag

Replace with conversion flag

(RUF010)


66-68: Abstract raise to an inner function

(TRY301)


66-68: Avoid specifying long messages outside the exception class

(TRY003)


74-74: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


74-74: Use explicit conversion flag

Replace with conversion flag

(RUF010)


89-91: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


89-91: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: index-backup-restore
🔇 Additional comments (3)
openrag/components/llm.py (1)

2-4: LGTM!

The json import is correctly added to support json.JSONDecodeError handling introduced in the error handling blocks.

openrag/routers/openai.py (2)

104-106: LGTM!

The trailing comma in the type annotation is a minor formatting improvement that helps with future diff readability.


200-228: Good approach for streaming error handling.

The pattern correctly addresses the streaming limitation noted in the PR comments: once a StreamingResponse with status 200 is sent, HTTP errors can't be propagated. Yielding an error chunk in OpenAI's error format followed by [DONE] is a sensible workaround.

The broad except Exception at line 217 is intentional here as a catch-all for streaming errors that cannot be communicated via HTTP status.

Comment thread openrag/routers/openai.py
Comment thread openrag/routers/openai.py Outdated
Comment thread openrag/routers/openai.py Outdated
@Ahmath-Gadji
Ahmath-Gadji force-pushed the fix/generic-400-errors branch from 736f17e to e36e196 Compare January 15, 2026 09:05
@paultranvan paultranvan added the fix Fix issue label Jan 15, 2026
paultranvan and others added 2 commits January 15, 2026 14:40
Extract error details from LLM API responses instead of showing
generic "Invalid JSON in API response" for all failures. Now displays
the actual error response from the LLM provider.

Fixes #197

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@paultranvan
paultranvan force-pushed the fix/generic-400-errors branch from 445c82f to d6f67ae Compare January 15, 2026 13:41
@paultranvan
paultranvan merged commit 8127032 into dev Jan 15, 2026
4 checks passed
@Ahmath-Gadji
Ahmath-Gadji deleted the fix/generic-400-errors branch January 16, 2026 14:12
This was referenced Feb 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Fix issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants