Skip to content

Start when the host has no embedding model configured yet - #17

Merged
jasperblues merged 1 commit into
mainfrom
fix/719-tolerate-absent-embedding-service
Aug 8, 2026
Merged

Start when the host has no embedding model configured yet#17
jasperblues merged 1 commit into
mainfrom
fix/719-tolerate-absent-embedding-service

Conversation

@jasperblues

Copy link
Copy Markdown
Collaborator

Problem

Both embedding-dependent beans resolved the default embedding service while being created:

DefaultMessageEmbedder(ai.withDefaultEmbeddingService())   // messageEmbedder
val embeddingService = ai.withDefaultEmbeddingService()    // chatStoreVectorIndexSchema

withDefaultEmbeddingService() throws when no embedding model is registered. A host whose provider key arrives at first run rather than at boot therefore could not start — and so could not reach the setup flow that would have supplied the key.

messageEmbedder — resolve lazily, then pin

LazyEmbeddingService resolves its delegate on first use and pins it thereafter.

Deferring keeps bean creation independent of two things: whether an embedding model exists at all, and the order in which provider configurations register their services. A host whose model arrives after startup picks up the real service on the first embedding call; a host with no model fails that call rather than the application context — and embedding failure is already non-fatal (see StoredConversation).

Pinning matters for provenance: a vector and the model name recorded alongside it are read separately, so a delegate that changed between the two reads would label a vector with the wrong model. A resolution that throws is deliberately not pinned, so a service that only becomes resolvable later is still picked up.

Resolution prefers the application's own EmbeddingService bean where there is an unambiguous one (getIfUnique(), so a @Primary bean counts), falling back to ai.withDefaultEmbeddingService() — which preserves today's behaviour rather than failing on ambiguity.

chatStoreVectorIndexSchema — skip rather than guess

Registers nothing when there is no model. An index built at the wrong dimension is worse than no index, because writes to it succeed — a real model configured later silently disagrees with everything already stored. Skipping is recoverable; a corrupted index is not obviously broken until someone notices bad results.

An absent-tolerant service may signal absence either by throwing or by reporting no dimensions, so both are treated as "no model".

Separately-owned catalogs

The constraint and vector catalogs are now named owners (CONSTRAINT_SCHEMA_OWNER, VECTOR_SCHEMA_OWNER).

This is load-bearing, not tidying. Catalogs sharing an owner are merged, and their versions with them. Leaving the constraint catalog unowned would version it by the embedding model — and drag that version to null on every boot where the vector catalog is skipped, churning schema state on hosts that are simply waiting for a key.

Compatibility

Unchanged for any deployment that has an embedding model — which is every existing one. The new paths are only reachable where resolution would previously have thrown and taken the context down.

Testing

  • Full suite: 93 pass, 0 failures (12 more than before this change)
  • ChatStoreSchemaWiringTest extended to cover the skip path and catalog ownership
  • Verified in situ by booting a consuming application with zero provider keys, which previously died here

Part of a four-repo fix for a host that boots before its provider key exists; the others are embabel/embabel-agent#1886, embabel/embabel-agent-rag-graph#16 and embabel/dice#77.

🤖 Generated with Claude Code

Both embedding-dependent beans resolved the default embedding service while
they were being created, via ai.withDefaultEmbeddingService(). That throws when
no embedding model is registered, so a host whose provider key arrives at first
run rather than at boot could not start — and could not reach the setup flow
that would have supplied the key.

messageEmbedder now holds a LazyEmbeddingService, which resolves its delegate
on first use and pins it thereafter. Deferring keeps bean creation independent
of both embedding-model availability and the order in which provider
configurations register their services; a host whose model arrives after
startup picks up the real service on the first embedding call. Pinning matters
for provenance: a vector and the model name recorded alongside it are read
separately, so a delegate that changed between the two reads would label a
vector with the wrong model. A resolution that throws is not pinned, so a
service that only becomes resolvable later is still picked up.

Resolution prefers the application's own EmbeddingService bean where there is
an unambiguous one (a @primary bean counts), falling back to the platform
default. A host that supports late configuration registers a service that
reports its own absence and can be switched on later.

chatStoreVectorIndexSchema registers nothing when there is no model, rather
than guessing a dimension. An index built at the wrong dimension is worse than
no index, because writes to it succeed and a real model later disagrees with
everything already stored. Absence is signalled either by throwing or by
reporting no dimensions, so both are treated as "no model".

The two catalogs are now separately owned. Catalogs sharing an owner are merged
and their versions with them, so leaving the constraint catalog unowned would
version it by the embedding model — and drag that version to null on every boot
where the vector catalog is skipped.

Behaviour is unchanged for any deployment that has an embedding model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jasperblues
jasperblues force-pushed the fix/719-tolerate-absent-embedding-service branch from ac81239 to 9f31fdc Compare August 7, 2026 23:51
@jasperblues
jasperblues merged commit 2e4a4dc into main Aug 8, 2026
8 checks passed
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