ADR-0046: Embedder interface and profile registry
Architecture decision record 0046 — deployment-time embedding profiles, Go/Python contracts, geometry validation, and stub-only M1 boundary.
ADR-0046: Embedder interface and profile registry
- Status: Accepted
- Date: 2026-08-23
- Authors: IBEX Harness team
- Milestone: 2.5.G4.M1 Embedder interface and registry
Context
Phase 3 memory write/search requires embedding vectors in pgvector. Different models produce incompatible coordinate systems (all-MiniLM-L6-v2 = 384-dim; BAAI/bge-m3 = 1024-dim). Mixing them in one index yields silent bad recall, not hard errors.
Track D (2.5.G4) needs a shared contract before TEI (G4.M2), hosted API (G4.M3), and content-hash cache (G4.M4).
Patterns to mirror: ADR-0025 (provider.Provider/Registry) and ADR-0043.
Decision
1) Deployment profile, not per-request selection
Profiles: cpu | gpu | hosted. Selected at deployment via IBEX_EMBEDDING_PROFILE (with IBEX_EMBEDDING_DIM / IBEX_EMBEDDING_MODEL). Per-request embedder selection is forbidden.
Documented default geometry:
| Profile | Default model | Dim |
|---|---|---|
cpu | all-MiniLM-L6-v2 | 384 |
gpu | BAAI/bge-m3 | 1024 |
hosted | OpenAI text-embedding-3-large / 3072 (Cohere 1024); G4.M3 | varies |
Never silently fall back across profiles (geometry change).
2) Go contract — packages/embedder
type Embedder interface {
Embed(ctx context.Context, texts []string) ([][]float32, error)
Name() string
ModelID() string
Dimensions() int
Profile() Profile
}- Registry keyed by
Profile(fail-closed on nil/duplicate/mismatch). ValidateGeometry(e, wantDim, wantModel)for startup.- Input limits:
MaxBatchTexts=64,MaxTextBytes=32KiB; empty batch rejected. - Outputs must be L2-normalized; callers validate length/finiteness.
- M1 ships a deterministic stub only (
Name()=="stub"). No TEI/OpenAI client in this package.
3) Python — services/embedder
EmbeddingBackend ABC mirrors Go (embed, name, model_id, dimensions, profile). Profile registry + stub + FastAPI /health//ready with startup geometry validation. Real backends land in G4.M2/M3.
4) Org columns vs deployment
ibex_core.organizations stores provisioned geometry defaults for new org rows:
embedding_profile,embedding_dim,embedding_model_id(migration defaults:cpu/ 384 /all-MiniLM-L6-v2)
The running embedder process is configured and validated only through IBEX_EMBEDDING_* at startup — it does not load org rows or derive geometry from deployment env beyond those variables. Per-org write-time mismatch enforcement (memory writes vs org geometry) is Phase 3 work.
5) Library vs inference process
Go package = shared contract for future Go callers. Python service = inference HTTP owner. TEI/OpenAI are backends behind the Python service (not an extra IBEX hop in front of TEI for M1).
6) Security / observability
Never log raw text or vector payloads. Log profile, model_id, batch size, latency, cache hit/miss (later), error class only.
7) Sequencing
| Milestone | Scope |
|---|---|
| G4.M1 (this) | Interface, registry, stub, org columns, contract tests |
| G4.M2 | TEI backend (gpu) |
| G4.M3 | Hosted API backend |
| G4.M4 | Content-hash cache (SHA-256 length-prefixed model_id+dim+text; {org_id}:embed:v1:{hex}) |
Consequences
Positive: Single contract before backends; fail-closed geometry; org schema ready for Phase 3.
Negative / follow-ups: Stub is not production inference on cpu; Voyage remains fail-closed; dual Go/Python contracts must stay aligned manually until codegen exists.
G4.M2 Update (2026-08-24)
G4.M2 has shipped the TEI GPU backend within this contract:
EmbeddingBackend.embed()converted toasync def(one contract break now, not three later).- Package layout refactored:
app/backends/,app/tei/,app/api/sub-packages. TEIBackend(name=="tei",profile=="gpu") backed byTeiClient(httpx, retries, jittered backoff).- Startup: fail-closed
/healthpoll +/infomodel-id geometry check; no gpu→stub fallback. POST /v1/embedinternal endpoint (BearerIBEX_EMBEDDING_API_TOKEN; probes stay unauthenticated).- Reference compose:
infra/reference/tei-embeddings.compose.yaml; Dockerfile atservices/embedder/Dockerfile. - Env:
IBEX_EMBEDDING_API_TOKEN,IBEX_EMBEDDING_TEI_BASE_URL,IBEX_EMBEDDING_TEI_ALLOW_INSECURE,IBEX_EMBEDDING_TEI_API_KEY,IBEX_EMBEDDING_TEI_TIMEOUT_SECONDS,IBEX_EMBEDDING_TEI_CONNECT_TIMEOUT_SECONDS,IBEX_EMBEDDING_TEI_MAX_RETRIES,IBEX_EMBEDDING_TEI_HEALTH_TIMEOUT_SECONDS— seeENVIRONMENT_VARIABLES.md. - M4 (cache decorator) plugs into the same
EmbeddingBackendABC without a further contract break.
G4.M3 Update (2026-08-24)
G4.M3 has shipped the hosted-API backend within this contract:
HostedAPIBackend(name=="openai"|"cohere",profile=="hosted") viaHostedClient(httpx, retries, jittered backoff).- OpenAI
POST /v1/embeddingsreusesparse_openai_compat_embed_response; CoherePOST /v2/embedis isolated inapp/hosted/protocol.py. - Fail-closed: hosted without
IBEX_EMBEDDING_HOSTED_API_KEYnever falls back to stub;voyageis accepted in settings and rejected at factory. - Startup probe embed confirms observed dimensions (OpenAI has no TEI
/info); mismatch blocks readiness. - Output is L2-normalized then re-validated (OpenAI does not guarantee unit norm).
- Env:
IBEX_EMBEDDING_HOSTED_PROVIDER,IBEX_EMBEDDING_HOSTED_API_KEY,IBEX_EMBEDDING_HOSTED_BASE_URL,IBEX_EMBEDDING_HOSTED_TIMEOUT_SECONDS,IBEX_EMBEDDING_HOSTED_CONNECT_TIMEOUT_SECONDS,IBEX_EMBEDDING_HOSTED_MAX_RETRIES; optional OpenAI-only aliasOPENAI_EMBEDDING_API_KEY. - CPU MiniLM remains stub (not part of M3). Cache decorator shipped in G4.M4.
G4.M4 Update (2026-08-24)
G4.M4 has shipped the Redis content-hash embedding cache within this contract:
CachingEmbeddingBackenddecorator wraps anyEmbeddingBackend; ABCembed(texts)unchanged.- Content address: SHA-256 of length-prefixed
(model_id, dim, utf-8 text)(ADR-0010; not MD5/xxHash/BLAKE3). - Redis keys are org-scoped:
{org_id}:embed:v1:{hex}(sketchembed:{model}:{hash}was rejected for tenancy / embedding inversion). POST /v1/embedrequiresorg_id(UUID); FastAPI sets a ContextVar for the decorator.- Fail-open on Redis errors at request time; fail-closed at startup when cache is enabled without Redis or PING fails.
- Values are
float32little-endian (<f4) bytes only (never pickle). Mixed batches use oneMGET+ one pipelineSET EX. Corrupt / non-finite / non-L2 blobs count as misses and are overwritten. - Prometheus:
ibex_embedder_cache_requests_total{backend,result}per text; authenticatedGET /metrics(BearerIBEX_EMBEDDING_API_TOKEN). JSONbackendstays the inner name (notcached:…). - Env:
IBEX_EMBEDDING_CACHE_ENABLED(default false),IBEX_EMBEDDING_CACHE_TTL_SECONDS(default 86400),IBEX_EMBEDDING_CACHE_REDIS_URL/REDIS_URL,IBEX_EMBEDDING_CACHE_REDIS_TIMEOUT_SECONDS. - Dependencies:
redis(redis-py ≥5 async, wheel-only / no hiredis) for MGET+pipeline;prometheus-clientfor scrape counters. Alternatives rejected: fakeredis-only (not CI proof), hiredis (native build), BLAKE3/xxHash (not ADR-0010). Both are pure-Python wheels compatible withuv --no-buildDocker builds; redis-py is the stdlib-adjacent client already used elsewhere in the org via go-redis patterns.
References
- ADR-0025 LLM provider abstraction
- ADR-0043 Tokenizer registry
- ENVIRONMENT_VARIABLES.md —
IBEX_EMBEDDING_* - Milestone 2.5.G4.M1
Was this page helpful?
Last updated on