Skip to content

Commit f78e9d9

Browse files
committed
fix: replace OpenAI SDK with raw HTTP to avoid Cloudflare WAF blocking, 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.
1 parent 602347d commit f78e9d9

9 files changed

Lines changed: 112 additions & 71 deletions

File tree

docker/entrypoint.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ set -e
44
echo "Running database migrations..."
55
/app/.venv/bin/python scripts/provision_db.py
66

7+
# Align vector dimensions after migration (migration hardcodes Vector(1536))
8+
echo "Configuring embedding dimensions..."
9+
/app/.venv/bin/python scripts/configure_embeddings.py --yes
10+
711
echo "Starting API server..."
812
exec /app/.venv/bin/fastapi run --host 0.0.0.0 src/main.py

src/crud/document.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from sqlalchemy import delete, select, update
77
from sqlalchemy.engine import CursorResult
8-
from sqlalchemy.exc import IntegrityError
8+
from sqlalchemy.exc import DataError, IntegrityError
99
from sqlalchemy.ext.asyncio import AsyncSession
1010
from sqlalchemy.sql import Select
1111
from sqlalchemy.sql.functions import func
@@ -893,6 +893,10 @@ async def create_observations(
893893
embeddings = await embedding_client.simple_batch_embed(contents)
894894
except ValueError as e:
895895
raise ValidationException(str(e)) from e
896+
except Exception as e:
897+
raise ValidationException(
898+
f"Failed to generate embeddings: {e}"
899+
) from e
896900

897901
# Create document objects and track embeddings for vector store
898902
honcho_documents: list[models.Document] = []
@@ -1039,10 +1043,10 @@ async def create_observations(
10391043
)
10401044
await db.commit()
10411045

1042-
except IntegrityError as e:
1046+
except (DataError, IntegrityError) as e:
10431047
await db.rollback()
10441048
raise ValidationException(
1045-
"Failed to create observations due to integrity constraint violation"
1049+
"Failed to create observations due to a data or integrity constraint violation"
10461050
) from e
10471051

10481052
logger.debug(

src/embedding_client.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
from collections.abc import Awaitable, Callable
77
from typing import Any, Literal, NamedTuple, TypeVar
88

9+
import httpx
910
import tiktoken
1011
from google import genai
1112
from google.genai import types as genai_types
1213
from nanoid import generate as generate_nanoid
13-
from openai import AsyncOpenAI
1414

1515
from .config import EmbeddingModelConfig, resolve_embedding_model_config, settings
1616

@@ -187,7 +187,7 @@ def __init__(
187187
if config.base_url
188188
else None
189189
)
190-
self.client: genai.Client | AsyncOpenAI = genai.Client(
190+
self.client: genai.Client | httpx.AsyncClient = genai.Client(
191191
api_key=config.api_key,
192192
http_options=http_options,
193193
)
@@ -198,9 +198,12 @@ def __init__(
198198
else: # openai
199199
if not config.api_key:
200200
raise ValueError("OpenAI API key is required")
201-
self.client = AsyncOpenAI(
202-
api_key=config.api_key,
203-
base_url=config.base_url,
201+
if not config.base_url:
202+
raise ValueError("OpenAI base URL is required")
203+
self._openai_api_key: str = config.api_key
204+
self._openai_base_url: str = config.base_url.rstrip("/")
205+
self.client = httpx.AsyncClient(
206+
timeout=httpx.Timeout(120.0, connect=30.0),
204207
)
205208
self.max_embedding_tokens = max_input_tokens
206209
self.max_batch_size = 2048 # OpenAI batch limit
@@ -257,14 +260,26 @@ async def _call_gemini() -> list[float]:
257260
fn=_call_gemini,
258261
)
259262

260-
openai_client = self.client
263+
http_client: httpx.AsyncClient = self.client # type: ignore[assignment]
261264

262265
async def _call_openai() -> list[float]:
263-
openai_kwargs: dict[str, Any] = {"model": self.model, "input": [query]}
266+
payload: dict[str, Any] = {"model": self.model, "input": [query]}
264267
if self.send_dimensions:
265-
openai_kwargs["dimensions"] = self.vector_dimensions
266-
response = await openai_client.embeddings.create(**openai_kwargs)
267-
return self._validate_embedding_dimensions(response.data[0].embedding)
268+
payload["dimensions"] = self.vector_dimensions
269+
resp = await http_client.post(
270+
f"{self._openai_base_url}/embeddings",
271+
json=payload,
272+
headers={
273+
"Authorization": f"Bearer {self._openai_api_key}",
274+
"Content-Type": "application/json",
275+
},
276+
)
277+
if not resp.is_success:
278+
raise RuntimeError(
279+
f"Embedding API error: {resp.status_code} {resp.text}"
280+
)
281+
data = resp.json()
282+
return self._validate_embedding_dimensions(data["data"][0]["embedding"])
268283

269284
return await _emit_embedding_call(
270285
provider=self.transport,
@@ -460,16 +475,29 @@ async def _call_provider() -> dict[str, dict[int, list[float]]]:
460475
self._validate_embedding_dimensions(embedding.values)
461476
)
462477
else: # openai
463-
openai_kwargs: dict[str, Any] = {
478+
http_client: httpx.AsyncClient = self.client # type: ignore[assignment]
479+
payload: dict[str, Any] = {
464480
"model": self.model,
465481
"input": [item.text for item in batch],
466482
}
467483
if self.send_dimensions:
468-
openai_kwargs["dimensions"] = self.vector_dimensions
469-
response = await self.client.embeddings.create(**openai_kwargs)
470-
for item, embedding_data in zip(batch, response.data, strict=True):
484+
payload["dimensions"] = self.vector_dimensions
485+
resp = await http_client.post(
486+
f"{self._openai_base_url}/embeddings",
487+
json=payload,
488+
headers={
489+
"Authorization": f"Bearer {self._openai_api_key}",
490+
"Content-Type": "application/json",
491+
},
492+
)
493+
if not resp.is_success:
494+
raise RuntimeError(
495+
f"Embedding API error: {resp.status_code} {resp.text}"
496+
)
497+
resp_data = resp.json()
498+
for item, emb in zip(batch, resp_data["data"], strict=True):
471499
result[item.text_id][item.chunk_index] = (
472-
self._validate_embedding_dimensions(embedding_data.embedding)
500+
self._validate_embedding_dimensions(emb["embedding"])
473501
)
474502
return result
475503

tests/alembic/revisions/test_b8183c5ffb48_codify_document_level_and_times_derived.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from nanoid import generate as generate_nanoid
88
from sqlalchemy import text
99

10+
from src.config import settings
1011
from tests.alembic.registry import register_after_upgrade, register_before_upgrade
1112
from tests.alembic.verifier import MigrationVerifier
1213

@@ -95,7 +96,7 @@ def prepare_codify_document_level_and_times_derived(
9596
"id": generate_nanoid(),
9697
"content": "Document with explicit level and times_derived=3",
9798
"metadata": json.dumps({"level": "explicit", "times_derived": 3}),
98-
"embedding": [0.1] * 1536,
99+
"embedding": [0.1] * settings.EMBEDDING.VECTOR_DIMENSIONS,
99100
"observer": OBSERVER_NAME,
100101
"observed": OBSERVED_NAME,
101102
"ws_name": WORKSPACE_NAME,
@@ -114,7 +115,7 @@ def prepare_codify_document_level_and_times_derived(
114115
"id": generate_nanoid(),
115116
"content": "Document with deductive level and times_derived=5",
116117
"metadata": json.dumps({"level": "deductive", "times_derived": 5}),
117-
"embedding": [0.2] * 1536,
118+
"embedding": [0.2] * settings.EMBEDDING.VECTOR_DIMENSIONS,
118119
"observer": OBSERVER_NAME,
119120
"observed": OBSERVED_NAME,
120121
"ws_name": WORKSPACE_NAME,
@@ -133,7 +134,7 @@ def prepare_codify_document_level_and_times_derived(
133134
"id": generate_nanoid(),
134135
"content": "Document without level or times_derived fields",
135136
"metadata": json.dumps({"other_field": "value"}),
136-
"embedding": [0.3] * 1536,
137+
"embedding": [0.3] * settings.EMBEDDING.VECTOR_DIMENSIONS,
137138
"observer": OBSERVER_NAME,
138139
"observed": OBSERVED_NAME,
139140
"ws_name": WORKSPACE_NAME,
@@ -152,7 +153,7 @@ def prepare_codify_document_level_and_times_derived(
152153
"id": generate_nanoid(),
153154
"content": "Document with only level field",
154155
"metadata": json.dumps({"level": "explicit"}),
155-
"embedding": [0.4] * 1536,
156+
"embedding": [0.4] * settings.EMBEDDING.VECTOR_DIMENSIONS,
156157
"observer": OBSERVER_NAME,
157158
"observed": OBSERVED_NAME,
158159
"ws_name": WORKSPACE_NAME,
@@ -171,7 +172,7 @@ def prepare_codify_document_level_and_times_derived(
171172
"id": generate_nanoid(),
172173
"content": "Document with only times_derived field",
173174
"metadata": json.dumps({"times_derived": 7}),
174-
"embedding": [0.5] * 1536,
175+
"embedding": [0.5] * settings.EMBEDDING.VECTOR_DIMENSIONS,
175176
"observer": OBSERVER_NAME,
176177
"observed": OBSERVED_NAME,
177178
"ws_name": WORKSPACE_NAME,
@@ -292,7 +293,7 @@ def verify_codify_document_level_and_times_derived(verifier: MigrationVerifier)
292293
"id": new_doc_id,
293294
"content": "New document after migration",
294295
"metadata": json.dumps({}),
295-
"embedding": [0.6] * 1536,
296+
"embedding": [0.6] * settings.EMBEDDING.VECTOR_DIMENSIONS,
296297
"observer": OBSERVER_NAME,
297298
"observed": OBSERVED_NAME,
298299
"ws_name": WORKSPACE_NAME,

0 commit comments

Comments
 (0)