Skip to content

fix: replace OpenAI SDK with raw HTTP to avoid Cloudflare WAF blocking, broaden exception handling#880

Open
hungcuong9125 wants to merge 4 commits into
plastic-labs:mainfrom
hungcuong9125:fix/conclusions-embedding-error-handling
Open

fix: replace OpenAI SDK with raw HTTP to avoid Cloudflare WAF blocking, broaden exception handling#880
hungcuong9125 wants to merge 4 commits into
plastic-labs:mainfrom
hungcuong9125:fix/conclusions-embedding-error-handling

Conversation

@hungcuong9125

@hungcuong9125 hungcuong9125 commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Fix POST /v3/workspaces/{workspace_id}/conclusions returning HTTP 500 due to embedding API failures and unhandled database exceptions.

Root Cause

The OpenAI Python SDK (AsyncOpenAI) automatically injects x-stainless-* telemetry headers into every request. When using a Cloudflare-proxied OpenAI-compatible endpoint (e.g., api.hungcuong.dev), Cloudflare's WAF blocks these non-standard headers, causing embedding generation to fail with PermissionDeniedError. Since the SDK's exception types (APIConnectionError, APIStatusError, etc.) are not subclasses of ValueError, they bypassed the existing except ValueError handler and propagated to the global exception handler as HTTP 500.

Secondary issue: a pgvector column dimension mismatch between the deployment config and migration default also caused an unhandled DataError on insert.

Changes

src/embedding_client.py — Replace OpenAI SDK with raw HTTP

Problem: AsyncOpenAI(api_key=..., base_url=...) sends x-stainless-arch, x-stainless-lang, x-stainless-package-version, x-stainless-runtime, x-stainless-runtime-version headers. Cloudflare WAF at common OpenAI-compatible reverse proxies rejects these.

Fix: Replace AsyncOpenAI with httpx.AsyncClient and make raw POST requests to {base_url}/embeddings with only Authorization and Content-Type headers. This applies to both embed() (single query) and _process_batch() (batch) code paths.

src/crud/document.py — Broaden exception handling

  • Add DataError alongside IntegrityError in the DB commit catch block — covers pgvector dimension mismatches.
  • Add a broad except Exception around the embedding call — any embedding failure (network, API, auth) now becomes a ValidationException (422) instead of 500.

docker/entrypoint.sh — Auto-align pgvector dimensions

Migration b8183c5ffb48 hardcodes Vector(1536). If the deployment config uses a different dimension, inserts fail with DataError. Added scripts/configure_embeddings.py --yes step at startup to ALTER the vector column to match the current config.

Test fixes

Replaced all hardcoded [0.x] * 1536 test vectors with [0.x] * settings.EMBEDDING.VECTOR_DIMENSIONS across 6 test files (47 occurrences) to match the runtime configuration.

Testing

  • Manual (all return HTTP 201):
    • Create conclusion with session_id
    • Create conclusion without session_id
    • Self-conclusion (observer_id == observed_id) ✅
  • Unit: 27/27 conclusion route tests pass.

Checklist

  • Code compiles and lints (ruff check, basedpyright)
  • Tests pass (pytest tests/routes/test_conclusions.py)
  • Docker build succeeds (docker compose build)
  • No breaking API changes

Summary by CodeRabbit

  • New Features

    • Startup now runs database migrations and then automatically configures embedding/vector dimensions to keep them aligned.
    • OpenAI embedding requests now use direct HTTP calls (instead of the SDK) for improved compatibility.
  • Bug Fixes

    • Expanded handling for embedding-generation failures (including network/HTTP errors) to return consistent errors.
    • Made the post-commit pgvector “mark synced” step safer by logging failures instead of interrupting the flow.
    • Broadened database constraint error handling during document insertion.
  • Tests

    • Updated test data and mocks to use configuration-driven embedding dimensionality (no more hardcoded 1536) and refactored OpenAI client tests to mock httpx.

…g, broaden exception handling, align vector dimensions at startup

Problem
- The OpenAI Python SDK (AsyncOpenAI) injects x-stainless-* telemetry headers into
  every request. Cloudflare-proxied OpenAI-compatible endpoints block these, causing
  embedding generation to fail with PermissionDeniedError. These exceptions are not
  ValueError subclasses, so they bypassed the except ValueError handler and became
  unhandled HTTP 500 errors.
- pgvector column dimension mismatch between migration default (1536) and runtime
  config caused unhandled DataError on insert.
- Missing broad exception catch around embedding calls in document CRUD.

Changes
- src/embedding_client.py: Replace AsyncOpenAI with raw httpx.AsyncClient POST
  requests, sending only Authorization and Content-Type headers.
- src/crud/document.py: Add DataError alongside IntegrityError; add broad
  except Exception catch around embedding call → ValidationException (422).
- docker/entrypoint.sh: Add scripts/configure_embeddings.py --yes step after
  migration to auto-align pgvector dimensions to current config.
- tests/: Update all hardcoded 1536-dimension test vectors to use
  settings.EMBEDDING.VECTOR_DIMENSIONS across 6 test files (47 occurrences).

Testing
- Manual: create conclusion with session_id, without session_id, self-conclusion
  all return HTTP 201 instead of 500.
- Unit: 27/27 conclusion route tests pass.
- CI: ruff check, basedpyright, pytest pass.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c9c7d16-0f7d-429e-b2f8-7ef24541346a

📥 Commits

Reviewing files that changed from the base of the PR and between 0b90d71 and 6c8c27b.

📒 Files selected for processing (1)
  • src/crud/document.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/crud/document.py

Walkthrough

The startup flow now runs embedding configuration after database provisioning. OpenAI embeddings use direct HTTP requests. Document CRUD broadens embedding and database error handling. Tests now use configured embedding vector dimensions instead of hardcoded 1536 values.

Changes

Configurable embedding dimensions and OpenAI HTTP client

Layer / File(s) Summary
Startup provisioning flow
docker/entrypoint.sh, scripts/provision_db.py
Updates the startup log and adds a post-migration embedding configuration command to the provisioning flow.
OpenAI embedding HTTP client
src/embedding_client.py
Replaces SDK-based OpenAI embedding calls with httpx requests, including single and batch embedding paths.
Document embedding and write error handling
src/crud/document.py
Broadens embedding-generation handling for provider and transport failures and expands rollback handling to include DataError.
Embedding client tests
tests/llm/test_embedding_client.py
Refactors OpenAI embedding tests to mock httpx.AsyncClient.post and assert request payloads and batching behavior.
Configured embedding dimensions in tests
tests/alembic/revisions/test_b8183c5ffb48_codify_document_level_and_times_derived.py, tests/crud/test_document.py, tests/deriver/test_vector_reconciliation.py, tests/routes/test_conclusions.py, tests/utils/test_agent_tools.py
Replaces hardcoded embedding lengths with settings.EMBEDDING.VECTOR_DIMENSIONS across test fixtures and seeded data.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EntryPoint as docker/entrypoint.sh
  participant ProvisionDB as scripts/provision_db.py
  participant ConfigureEmbeddings as scripts/configure_embeddings.py
  EntryPoint->>ProvisionDB: start provisioning
  ProvisionDB->>ProvisionDB: run database initialization
  ProvisionDB->>ConfigureEmbeddings: execute --yes
  ConfigureEmbeddings-->>ProvisionDB: stdout, stderr, exit code
  ProvisionDB-->>EntryPoint: continue startup
Loading

Possibly related PRs

  • plastic-labs/honcho#678: Both PRs wire configurable embedding dimensionality into startup, client, and test code, and this PR also invokes the embedding configuration script.

Suggested reviewers: Rajat-Ahuja1997, erosika

Poem

A bunny hopped through setup light,
With embeddings tuned just right.
HTTP paws tapped httpx quick,
And vectors matched the config stick. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main code changes: switching OpenAI embedding calls to raw HTTP and broadening exception handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/crud/document.py (1)

896-899: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Broad except Exception conflates client errors with transient/infra failures.

Wrapping any embedding-call exception (timeouts, connection resets, provider 5xx/rate-limit errors) as a ValidationException (422) tells the client their input was invalid, when the real cause may be a transient dependency failure the client should retry rather than "fix." This trades one wrong status code (500) for another kind of misleading one (422).

Consider catching narrower transport/HTTP exception types (e.g., from httpx) and mapping them to a more appropriate status (e.g., 503) while reserving ValidationException for genuinely malformed input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/crud/document.py` around lines 896 - 899, The broad exception handling
around the embedding generation block in document CRUD is mapping all failures
to ValidationException, which incorrectly treats transport/provider outages as
bad input. Narrow the except path around the embedding call to catch only
genuine validation/malformed-input cases for ValidationException, and handle
HTTP/transport failures separately in the relevant embedding helper or method
with a more appropriate retryable/server-side exception (for example, using the
document embedding flow in src/crud/document.py and the embedding-call wrapper
that raises the current error).
src/embedding_client.py (1)

263-282: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate request-building/error-handling logic between single and batch embed paths.

embed() and the OpenAI branch of _process_batch() both construct the same headers dict, POST to the same endpoint, check resp.is_success, and parse resp.json()["data"] almost identically. Consider extracting a small private helper (e.g. _openai_post_embeddings(http_client, payload)) to avoid maintaining two copies of this logic (e.g., if auth header format or error message ever needs to change).

♻️ Proposed extraction
+    async def _openai_post_embeddings(
+        self, http_client: httpx.AsyncClient, payload: dict[str, Any]
+    ) -> dict[str, Any]:
+        resp = await http_client.post(
+            f"{self._openai_base_url}/embeddings",
+            json=payload,
+            headers={
+                "Authorization": f"Bearer {self._openai_api_key}",
+                "Content-Type": "application/json",
+            },
+        )
+        if not resp.is_success:
+            raise RuntimeError(f"Embedding API error: {resp.status_code} {resp.text}")
+        return resp.json()

Then call data = await self._openai_post_embeddings(http_client, payload) from both embed() and _process_batch().

Also applies to: 478-500

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/embedding_client.py` around lines 263 - 282, The OpenAI embedding request
logic is duplicated in both embed() and the OpenAI branch of _process_batch(),
including headers, POST request, success check, and JSON parsing. Extract this
into a small private helper such as _openai_post_embeddings(http_client,
payload) inside EmbeddingClient, then have both embed() and _process_batch()
call it so the auth header, endpoint, and error handling stay in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docker/entrypoint.sh`:
- Around line 7-10: The startup script currently runs the DDL-capable
configure_embeddings.py --yes step unconditionally in entrypoint execution,
which can race across multiple replicas. Move this schema-altering embedding
configuration out of the container entrypoint and into a one-off migration/init
job or equivalent singleton startup path, using the configure_embeddings.py flow
only where it will run once per deployment rather than per app replica.

In `@src/crud/document.py`:
- Around line 1046-1049: The shared except block in the document creation flow
is too broad because it also catches DataError/IntegrityError from later
sync-state commits, causing a misleading “Failed to create observations” message
after the documents have already been committed. Narrow the handler around the
initial insert in the document creation path, or update the ValidationException
message in the create_observations logic to explicitly cover both document
creation and sync-state updates so the error text matches where the failure can
occur.

In `@src/embedding_client.py`:
- Line 9: The OpenAI test monkeypatches are targeting
src.embedding_client.AsyncOpenAI, but that symbol no longer exists and causes
AttributeError during test setup. Update the helpers in
tests/llm/test_embedding_client.py, especially _build_openai_client and related
setup, to patch httpx.AsyncClient behavior or use an injected fake client
instead of monkeypatching AsyncOpenAI. Keep the test coverage focused on the
embedding client’s request/response handling through the actual
httpx.AsyncClient path.

---

Nitpick comments:
In `@src/crud/document.py`:
- Around line 896-899: The broad exception handling around the embedding
generation block in document CRUD is mapping all failures to
ValidationException, which incorrectly treats transport/provider outages as bad
input. Narrow the except path around the embedding call to catch only genuine
validation/malformed-input cases for ValidationException, and handle
HTTP/transport failures separately in the relevant embedding helper or method
with a more appropriate retryable/server-side exception (for example, using the
document embedding flow in src/crud/document.py and the embedding-call wrapper
that raises the current error).

In `@src/embedding_client.py`:
- Around line 263-282: The OpenAI embedding request logic is duplicated in both
embed() and the OpenAI branch of _process_batch(), including headers, POST
request, success check, and JSON parsing. Extract this into a small private
helper such as _openai_post_embeddings(http_client, payload) inside
EmbeddingClient, then have both embed() and _process_batch() call it so the auth
header, endpoint, and error handling stay in one place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e025854b-a406-47db-8d18-eafeb2634433

📥 Commits

Reviewing files that changed from the base of the PR and between 602347d and f78e9d9.

📒 Files selected for processing (9)
  • docker/entrypoint.sh
  • src/crud/document.py
  • src/embedding_client.py
  • tests/alembic/revisions/test_b8183c5ffb48_codify_document_level_and_times_derived.py
  • tests/crud/test_document.py
  • tests/deriver/test_vector_reconciliation.py
  • tests/llm/test_embedding_client.py
  • tests/routes/test_conclusions.py
  • tests/utils/test_agent_tools.py

Comment thread docker/entrypoint.sh Outdated
Comment thread src/crud/document.py Outdated
Comment thread src/embedding_client.py
…ty, helper extraction, test fix

Changes per review:
1. docker/entrypoint.sh — Move DDL configure_embeddings.py --yes into
   scripts/provision_db.py (DB init runs once) via subprocess.run non-fatal,
   avoiding race condition on ALTER COLUMN across replicas.
2. scripts/provision_db.py — Add non-fatal subprocess.run call to
   configure_embeddings.py --yes after alembic migration completes.
3. src/crud/document.py — Split try/except: document insert wrapped with
   (DataError, IntegrityError) → ValidationException; sync-state updates
   moved outside the insert try/except block. Broaden embedding-error
   handling: ValueError → 422, httpx.HTTPStatusError → 503,
   httpx.ConnectError/TimeoutException/RemoteProtocolError → 503,
   generic Exception → 500.
4. src/embedding_client.py — Extract _openai_post_embeddings(http_client,
   payload) helper to eliminate duplicate POST logic between embed()
   and _process_batch().
5. tests/llm/test_embedding_client.py — Replace monkeypatched
   AsyncOpenAI with monkeypatched httpx.AsyncClient.post using
   httpx.Response fixtures, fixing AttributeError from removed class.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/llm/test_embedding_client.py (1)

315-326: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate fake_post mocking logic across tests.

Three tests (test_simple_batch_embed_respects_token_budget_per_request, test_simple_batch_embed_rejects_oversized_input, test_prepare_chunks_returns_ordered_chunks) reimplement inline fake_post/client-construction logic that closely mirrors _build_openai_client. Consider extending _build_openai_client (e.g. optional max_input_tokens/max_tokens_per_request params) to cover these cases and reduce duplicated mock setup.

Also applies to: 353-356, 379-382

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/llm/test_embedding_client.py` around lines 315 - 326, The three
embedding-client tests are duplicating the same `fake_post` and client setup
logic instead of reusing the shared helper. Update `_build_openai_client` in the
embedding client test utilities to accept the needed optional parameters such as
`max_input_tokens` and/or `max_tokens_per_request`, then switch
`test_simple_batch_embed_respects_token_budget_per_request`,
`test_simple_batch_embed_rejects_oversized_input`, and
`test_prepare_chunks_returns_ordered_chunks` to use that helper so the mocked
`httpx.AsyncClient.post` setup is centralized.
src/crud/document.py (1)

859-862: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring Raises section is now incomplete.

The new embedding-failure paths (Lines 898-912) raise HonchoException for provider HTTP errors and transient connectivity issues, but the docstring only documents ResourceNotFoundException and ValidationException.

📝 Proposed docstring update
     Raises:
         ResourceNotFoundException: If any session or peer is not found
-        ValidationException: If embedding generation fails or integrity constraint is violated
+        ValidationException: If embedding validation fails or a data/integrity constraint is violated
+        HonchoException: If the embedding provider returns an error or is unreachable

As per coding guidelines, "In Python code, use Google-style docstrings."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/crud/document.py` around lines 859 - 862, Update the Google-style
docstring for the method in document.py that handles embedding generation so the
Raises section includes HonchoException alongside ResourceNotFoundException and
ValidationException. Add a brief note that HonchoException is raised for
provider HTTP errors and transient connectivity failures in the
embedding-generation paths, and keep the documentation aligned with the new
error behavior in the embedding workflow.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/crud/document.py`:
- Around line 977-988: The pgvector-only sync-state update in the document flow
is missing the same error handling used in the external-vector-store branch, so
a transient failure in db.execute or db.commit can still bubble up as a 500.
Wrap the no-external-vector-store path in the same kind of try/except handling
used around upsert_many/db.execute/commit for the external branch in
src/crud/document.py, and make sure the exception path logs the failure and
returns a controlled response instead of propagating it.

---

Nitpick comments:
In `@src/crud/document.py`:
- Around line 859-862: Update the Google-style docstring for the method in
document.py that handles embedding generation so the Raises section includes
HonchoException alongside ResourceNotFoundException and ValidationException. Add
a brief note that HonchoException is raised for provider HTTP errors and
transient connectivity failures in the embedding-generation paths, and keep the
documentation aligned with the new error behavior in the embedding workflow.

In `@tests/llm/test_embedding_client.py`:
- Around line 315-326: The three embedding-client tests are duplicating the same
`fake_post` and client setup logic instead of reusing the shared helper. Update
`_build_openai_client` in the embedding client test utilities to accept the
needed optional parameters such as `max_input_tokens` and/or
`max_tokens_per_request`, then switch
`test_simple_batch_embed_respects_token_budget_per_request`,
`test_simple_batch_embed_rejects_oversized_input`, and
`test_prepare_chunks_returns_ordered_chunks` to use that helper so the mocked
`httpx.AsyncClient.post` setup is centralized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8571742b-27d4-427d-9119-840a5da7b79c

📥 Commits

Reviewing files that changed from the base of the PR and between f78e9d9 and ba163d6.

📒 Files selected for processing (5)
  • docker/entrypoint.sh
  • scripts/provision_db.py
  • src/crud/document.py
  • src/embedding_client.py
  • tests/llm/test_embedding_client.py
✅ Files skipped from review due to trivial changes (1)
  • docker/entrypoint.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/embedding_client.py

Comment thread src/crud/document.py Outdated
…cstring, wrap pgvector sync in try/except

Changes:
- tests/llm/test_embedding_client.py: Extend _build_openai_client helper with
  optional max_input_tokens/max_tokens_per_request params and refactor 3 tests
  (test_simple_batch_embed_respects_token_budget_per_request,
  test_simple_batch_embed_rejects_oversized_input,
  test_prepare_chunks_returns_ordered_chunks) to use it, eliminating duplicate
  fake_post mocking logic.
- src/crud/document.py: Update Google-style docstring Raises section to include
  HonchoException for provider HTTP errors and transient connectivity failures.
  Wrap the pgvector-only sync-state update path (no external vector store) in
  try/except with logger.exception to prevent transient DB failures from
  bubbling up as HTTP 500.
…r path

- Add await db.rollback() inside except block so the failed transaction
  is properly rolled back on transient DB errors.
- Include workspace_name in the log message for better debugging context.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant