AI Embed — Unified Embeddings Foundation
- Area: Intelligence
- Path:
services/ai/embed - Kind: Embedding generation foundation (text + image + audio; intent-based model selection)
- Status: v0.0.7 —
#013binary naming alignment shipped 2026-05-11. Daemon binary renamedkembed→koder-embed; workerkembed-worker→koder-embed-worker; CLIkembed-admin→kembed. Matches the cache precedent (koder-cachedaemon +kcacheCLI). systemd units updated; feature surface unchanged from v0.0.6. Previous v0.0.6:#007HTTP cache adapter consumingservices/ai/cache#008KV API.
Role in the stack
embed consolidates embeddings server-side. Today every consumer ships its own model (kortex, search, recsys, agents) — no shared cache, inconsistent dimensions, no consolidated billing. Migrating to a better model means rewriting each consumer.
The headline design choice: intent-based selection. Consumers query by intent (text-match-search, text-classify, image-match) and the registry picks the model. Swapping a model is a registry change, not a consumer rewrite.
It is the Koder analog of OpenAI Embeddings, Voyage, Cohere Embed and Jina — self-hosted via BGE / E5 / CLIP / CLAP on GPU runtime, with proxy fallback to OpenAI / Voyage / Cohere through services/ai/gateway for capability gaps.
Boundary vs neighbors
infra/data/kdb-vectoris the vector store (search, ANN);embedis the producer.services/ai/cacheis the embedding cache backend (keyed by model+input hash).services/ai/runtimehosts the model weights.
Features (v1 target)
- Text embeddings: BGE-multilingual default (pt+en), BGE-large-en opt-in, E5, Instructor
- Image embeddings: CLIP-ViT-L/14 + SigLIP for cross-modal
- Audio embeddings: CLAP for audio-text cross-modal
- Intent registry: 6 default intents, per-tenant override
- Batch coalescing: 10ms window OR 64-input cap for throughput
- Cache integration: hits don't decrement quota
Primary couplings
| Consumer | Relationship |
|---|---|
services/ai/memory |
Episodic + semantic memory backed by embeddings |
services/ai/search |
Hybrid lexical+semantic search |
services/ai/recsys |
User+item embedding for similarity |
services/ai/kortex |
Knowledge-graph node + edge embeddings |
services/ai/agents |
Tool selection + memory retrieval |
services/ai/classify |
Zero-shot classification via embedding similarity |
services/ai/extract |
Embedding-based entity disambiguation |
services/ai/cache |
Embedding-result cache |
services/ai/runtime |
Local model serving |
infra/data/kdb-vector |
Native vector store target |
RFC and bootstrap
- RFC:
embed-RFC-001-foundations.kmd— accepted 2026-05-09 - Bootstrap ticket:
services/ai/backlog/done/121-embed-bootstrap.md - API contract (normative):
api-v1.kmd+intents.toml— shipped 2026-05-11 via#001(5 endpoints, intent-over-model design, 7 registered models across textimageaudio, error map) - Implementation tickets:
done/:001(API contract + intent registry),002(Go server skeleton + coalescer),003(quotas + cache + audit + admin API),004(split into #009-#012),006(kembed-admin CLI),007(HTTP cache adapter — consumes cache#008),008(superseded by #010),010(real tokenizers — WordPiece pure-Go),013(binary naming alignment) — all 2026-05-11pending/:005(image+audio CLIP/CLAP),009(ONNX runtime + LocalONNXWorker — CGO),011(GPU LXCs.embed.gpuprovisioning),012(weights + MTEB benchmark — blocked-by #009 + #011)
Recent changes
- 2026-06-24 (#021 — DEV deploy LIVE) — embed is deployed + running in the fleet. LXC
embed@ 10.0.1.146 on s.khost1:koder-embedserver (:18089) +koder-embed-worker(:18091,runtime=onnx:cgo, real multilingual XLM-R model + SentencePiece) under systemd; onnxruntime + model provisioned to/opt/onnxruntime+/var/lib/koder-embed/models/. Validated in-fleet:curl /v1/embed/text→ HTTP 200, pt-BR cosine 0.9782/0.1152. Registered inkoder-service-ports.toml. Publicembed.koder.devdeliberately deferred:dev_tokens_enabledaccepts anydev-<t>-<u>bearer → public exposure would be an open API (security.kmd); embed is internal (its consumermemoryreaches10.0.1.146:18089internally), so the high-blast-radius Jet edit + DNS wait on prod JWKS auth + a real external consumer. Reproducible recipe inbackend/deploy/(provision-onnx.sh+ units +config.example.toml). Remaining (#021): prod auth, then public proxy + memory prod config +services/ai/cache. - 2026-06-24 (service-path e2e +
/k-arch) — Production topology validated:memory→ embed service → real embeddings./k-archratified the service path over an in-process embedder: a memory-local ONNX model is the exact anti-patternembedexists to eliminate (no shared cache, inconsistent dims, no consolidated billing, model-swap = consumer rewrite).services/ai/memory#012(in-process) re-gatedowner-decision; the offline case is akoder-embed-workersidecar, not a fork. End-to-end proof (off-laptop dev-linux-id): the realmemory.recall.ServiceEmbedder→ livekoder-embed/v1/embed/text(Bearerdev-<tenant>-<user>,{inputs,intent}) → HTTP →koder-embed-worker(onnxcgo) → SentencePiece + real ONNX (paraphrase-multilingual-MiniLM, mean) → pt-BR cosine(paraphrase)0.9782 / unrelated0.1152, identical to the direct worker test (the HTTP wire preserves the vectors). Two processes: server (plain build) + worker (-tags onnxcgo+libonnxruntime.so), wired via[[workers]][[onnx_models]][[tokenizers]]+ a[registry]intents map. Remaining = #021 (real deployment to devprd + prod auth + pointmemoryat the endpoint + `servicesai/cache`). +1 live regression test (env-gated). - 2026-06-24 (#019 ship) — SentencePiece Unigram tokenizer (pure Go) → real multilingual (pt-BR) embeddings. The worker was WordPiece-only (BERTBGE-en); the multilingual embedders Koder products need (bge-m3, multilingual-E5, paraphrase-multilingual-MiniLM) all use SentencePiece Unigram (XLM-R, 250k vocab). `internaltokenizersentencepiece.go
(FromSentencePieceJSON, rejects non-Unigram) loads a HFtokenizer.jsonand runs the XLM-R pipeline: **NFKC** normalize (approximation of the reference "Precompiled" charmap — byte-exact deferred to **#020**) → **WhitespaceSplit + Metaspace** (▁per word) → **Unigram Viterbi** (DP maximizing summed log-probscores; unmatched runes →unk) →s … <s>specials (default,WithSPSpecialTokens(false)opt-out). Wired intoworker.LoadTokenizersaskind = "sentencepiece"(vocab_path→tokenizer.json). **Verified off-laptop (dev-linux-id):** unit (TestSentencePieceUnigramViterbiAndSpecials+ non-Unigram rejection) +go vetgreen; onnxcgo e2e with real **paraphrase-multilingual-MiniLM-L12-v2** (mean pooling) on **pt-BR** throughworker.Embed→ **cosine(paraphrase)=0.9782 / unrelated=0.1152** — excellent semantic separation, the NFKC approximation is more than sufficient. +2 tests. Unblocks quality multilingual embeddings via the embed service + clearsmemory#012`'s SentencePiece blocker. Follow-on #020 (byte-exact normalizer parity). - 2026-06-24 (#018 ship) — koder-embed-worker serves real embeddings on CPU (wires #017's Session into the worker). New
[[onnx_models]]config (config.OnnxModelEntry:model_path,pooling,intra_op_threads) — deployment binding, mirrors[[tokenizers]](the path is infra, not public catalog).buildBackendbuildsmap[id]worker.ONNXModelConfigand passesworker.NewONNXAdapterFactory(map)toNewLocalONNXWorker; the factory resolves per-modelmodel_path+ pooling (poolKindFor:"cls"→PoolCLS), and under the real build (onnx.BuildHasONNX) a model with no entry is a hard error (modelpath required — mirrors the strict-tokenizer contract). *wo correctness gaps found+fixed that a naive "pass modelpath" would have shipped broken: (A) theencodeTokenizerbridge fed raw WordPiece ids with no[CLS][SEP]*→ CLS pooling read the wrong vector at position 0; fixed at root —WordPiece.Encodenow wraps[CLS] … [SEP]by default (HFadd_special_tokens=True, ids from vocab,WithSpecialTokens(false)opt-out); (B) BERTBGE graphs requiretoken_type_ids→Runerrored "Missing Input";GenericPoolingAdapternow feeds all-zerotoken_type_ids(the Session forwards only graph inputs it has, harmless for E5). Build: the worker built-tags onnxcgoneedslibonnxruntime.soat runtime (CGO_CFLAGS=-I<ort>/include CGO_LDFLAGS="-L<ort>/lib -lonnxruntime" LD_LIBRARY_PATH=<ort>/lib; onnxruntime-linux-x64-1.20.1 prebuilt). Verified off-laptop (dev-linux-id): defaultmock +go vetgreen; onnxcgo + real bge-small-en-v1.5 —TestLocalONNXWorkerRealBGEEmbeddingsparaphrase 0.8455 / unrelated 0.3924 throughworker.Embed, matching the raw #017 Session exactly (full tokenizer→factory→Session→adapter chain). +3 tests (2 mock unit + 1 onnxcgo e2e). Follow-on: realmodels.tomlin the deployISO pipeline, multilingual SentencePiece,memory#012consumer, CUDA EP (#011). - 2026-06-24 (#017 ship) — Real onnxruntime CGO Session on CPU — the runtime is no longer mock-only.
#009shipped just theonnx.Sessioninterface + pure-GoMockSession+ a skeletonloader_cgo.goreturningErrNotImplemented; the real CGO bindings were deferred "to land with #011 (GPU LXC)". That conflated two independent axes — CPU small-model inference (bge-small dim384; the literalmemory#012target "batch=32 <200ms on CPU"; platform linux x86-64 already fixed) vs GPU large-model serving (BGE-large p50<50ms; needs #011 + CUDA EP).#017implements the CPU slice (theonnx.Sessioninterface already abstracts the device → CUDA later is an EP swap, not a rewrite; stays sequenced with #011).loader_cgo.go(//go:build onnxcgo) wraps the onnxruntime C API (OrtApifunction-pointer table) via C-preamble helpers:ort_open(env + session opts + CPU mem, intra-op threads, graph-opt ALL, allOrtStatuschecked), graph name introspection, and a statefulRunCtxthat copies int64 input buffers (CreateTensorWithDatadoesn't copy → copy outlivesRun) →Run→ float32 outputs.realSessionimplementsSession(LoadRunClose, mutex + finalizer); scope v1 = int64 inputs / float32 outputs (the text-path dtypes). Default!onnxcgobuild unchanged (mock) → normal CI needs no C library. Validated off-laptop (dev-linux-id, onnxruntime 1.20.1 CPU, real BAAI/bge-small-en-v1.5): tokenize via WordPiece, CLS-pool + L2-norm → paraphrase cosine 0.8455 ranks far above unrelated 0.3924 (>0.7) — the runtime is real. Unblocks the whole Stack's real-embedding capability (incl.memory#012). Follow-ons: #018 (wire real Session intoLocalONNXWorker+ real-model config), SentencePiece for multilingual (XLM-R),memory#012consumer, CUDA EP (#011). +1 real-model regression test (build-tag + env gated). - 2026-06-02 (#015 ship) — Real per-model tokenizers wired into the ONNX worker.
worker.LoadTokenizers(cfg.Tokenizers)builds thetext.Tokenizerset from[[tokenizers]]via anencodeTokenizerbridge (internal/tokenizer.Tokenizersingle-textEncode→[]int→ batchinput_ids+attention_maskfor the adapter, padding handled byGenericPoolingAdapter). Strict: a wordpiece entry with a missing vocab is a startup error (tokenizer: <id> not found at <path>), not a silent passthrough — passthrough now only backs models with no entry (stubmock). Wired intobuildBackend's"onnx"path, replacing the empty placeholder. Only WordPiece (vocab.txt) is wired — SentencePieceBPE (tokenizer.json) still need those tokenizers added tointernal/tokenizer. #016 (Instructor prefix) is blocked on a registry-schema gap (noModel.Family/"instructor" source) — deferred. +4 tests. - 2026-05-11 (#013 ship) — Binary naming aligned with cache precedent. Daemon binary:
kembed→koder-embed(matchescmd/koder-embed/+ systemd unit). Worker:kembed-worker→koder-embed-worker. CLI:kembed-admin→kembed(now matches thebinaries-and-cli/naming.kmd"binary = aliases[0] || slug" rule). systemd units'ExecStartpaths updated to/usr/local/bin/koder-embed[-worker]. Zero behaviour change (pure rename); 65+ tests stay green. Breaking for any operator running pre-v0.0.7 packages on the same host —/usr/local/bin/kembednow refers to the CLI, not the daemon. - 2026-05-11 (#007 ship) — HTTP cache adapter shipped.
internal/cache/http_adapter.HTTPCacheconsumesservices/ai/cache#008public KV API (PUT/GET/DELETE /v1/cache/kv/{tenant}/{key}) with circuit breaker (5 consecutive 5xx/network errors → open for 30s, single probe re-closes on success), per-call timeout (default 1s), TTL via?ttl_seconds=(default 30 days, clamped server-side), defensive empty-BaseURL → NoOp. Wire format JSON{vector: [...], usage: {text_tokens, image_count, audio_seconds}}. 9 tests (HC1-HC9) including bearer header propagation, URL-encoding of|-keys, 404=clean-miss (no trip), trip+cooldown reopen, local Stats accumulation.[cache.backend] = "http"selects it;[cache.http]config section in TOML. - 2026-05-11 (#006 ship + #007 blocker) —
kembed-adminCLI shipped (cmd/kembed/,bin/kembed-admin). Subcommandsstats,quotas {list,set,delete},models. Mirrorskcachepattern but binary namedkembed-admin(notkembed) to avoid clash with the daemon binarykembedfrom#002— naming alignment tracked as#013. Cross-sector discovery:#007(HTTP cache adapter) was blocked becauseservices/ai/cacheexposes no public KV PUTGET — only admin + middleware Wrap. New ticket `servicesai/cache#008(public KV HTTP API) opened; embed#007` re-stated as blocked-by until that lands. - 2026-05-11 (#004 split + #010 ship) —
#004was bundling code + tokenizers + GPU LXC + weights + benchmarks; split into 4 sub-tickets (#009ONNX runtime + LocalONNXWorker (CGO),#010real tokenizers (this commit),#011GPU LXCs.embed.gpuprovisioning,#012weights + MTEB benchmark).#010shipped:internal/tokenizer/with BERT-canonical WordPiece in pure Go (greedy longest-prefix,##continuations, CJK rune-per-piece, lowercase toggle, max-chars fallback), Whitespace fallback,RegistrymodelID→Tokenizer index,[[tokenizers]]TOML config, handler swapsapproxTokens→tokenizerFor(model.ID).Count. Quota billing now charges in target-model tokens. SentencePiece deferred to#009/#012.#008closed as superseded-by#010. +13 tests (T1-T12 unit + I11 integration). - 2026-05-11 (#003) — Multi-tenant quotas + per-input cache + audit.
internal/quota.Enforcerwith day-rolling UTC counters for text-tokens / image-count / audio-seconds, per-tenant override that shadows the default,AllowedModels+BatchMaxSizechecks,ReserveRefundclamp-at-zero. `internalcachewithNoOp+Memory(LRU bound, tenant-scoped key tuple).internalauditslog wrapper. Admin APIGETPOSTDELETE /v1embedquotas[{tenant}]+GET v1embed/stats(admin-gated). Cache hits skip quota by construction (handler reserves only the miss bundle). Estimated-vs-actual reconciliation refunds overshoots post-dispatch. +26 tests (Q1-Q10, C1-C6, I1-I10). TTL deferred to#007` HTTP cache adapter. - 2026-05-11 (#002) — Go server skeleton shipped (
backend/cmd/koder-embed+backend/cmd/koder-embed-worker). Wire format: HTTPJSON server↔worker (`POST /v1workerembed); gRPC deferred. Coalescer: per-(modality, model_id) queue, 10 ms window OR 64-input cap. Stub worker uses xxh3-seeded deterministic vectors (real ONNX in#004`#005). Auth + identity context split intointernal/identctx/so handlers can read the principal without an auth↔handler import cycle. 24 tests pass (R1-R7,C1-C7,H1-H8,S1-S2).
Self-hosted-first analysis (5 gates)
| Gate | Status | Notes |
|---|---|---|
| G1 Feature parity | pending | Skeleton phase; BGEE5CLIPCLAP cover textimage/audio self-hosted |
| G2 Performance | pending | Target BGE-large p50 < 50ms/batch=8 short sentences on RTX 4090 |
| G3 Stability | pending | Pre-MVP |
| G4 Capability | pending | Multimodal (text+image+audio) covered; specialized intents may proxy initially |
| G5 Critical-path readiness | pending | Pre-MVP; consolidating kortexsearchrecsys is the first concrete unblock |