Skip to content

feat: provider "custom" loads a user embedder factory from settings - #316

Open
jkbdco wants to merge 1 commit into
cocoindex-io:mainfrom
jkbdco:embedder-hook
Open

jkbdco wants to merge 1 commit into
cocoindex-io:mainfrom
jkbdco:embedder-hook

Conversation

@jkbdco

@jkbdco jkbdco commented Oct 22, 2026 •

Copy link
Copy Markdown

Summary

Adds a third embedder path beside sentence-transformers and litellm: provider: custom with embedding.embedder: "module.path:callable". cocoindex-code imports the factory and calls it once at daemon start as factory(settings, indexing_params); the result must expose an async embed() and cocoindex's __coco_vector_schema__() (checked at load against a small runtime protocol, CustomEmbedder). The form mirrors the module.path:callable strings the chunker registry already accepts.

Why

The cloud path routes every embedding through litellm. That is the right default for a tool that wants to support a hundred providers out of the box, but it is also a large dependency tree (aiohttp, boto3, openai, tiktoken, tokenizers and about forty more), and litellm has had two PyPI supply-chain compromises this year (1.82.7/1.82.8 in March, 1.107.0 on October 21). A user who talks to one provider can now install a small adapter of their own next to the tool (uv tool install cocoindex-code --with my-embedders, with a uv override to leave litellm out) and keep the rest of cocoindex-code unchanged. We run the index this way against Voyage AI; the adapter is a single-file httpx client.

Changes

  • settings.py: EmbeddingSettings.embedder, validated in __post_init__ (required with custom, rejected with any other provider), round-tripped by the YAML (de)serialisers; a comment template for the custom provider in generated settings.
  • embedder_params.py: validate_params passes a custom embedder's kwargs through; the embedder owns them.
  • shared.py: CustomEmbedder protocol, load_custom_embedder_factory() with errors that name the spec (malformed value, import failure, missing attribute, non-callable), and the custom branch in create_embedder() that checks the returned object against the protocol.
  • tests/test_custom_embedder.py: settings validation and round trip, params pass-through, loading a factory from a temporary module, contract rejection, every loader error, and an embed + schema call through the loaded object.
  • EMBEDDINGS.md: a "Custom Embedder" section with the YAML shape and the contract; CLAUDE.md: one line in the embedding-flow notes.

ccc init is unchanged and does not offer the provider; it is a settings-file feature.

Testing

  • tests/test_custom_embedder.py, tests/test_settings.py, tests/test_embedder_params.py: 100 passed.
  • Full suite in a venv without sentence-transformers: 30 failures, identical to main run the same way in the same environment (all need sentence_transformers/torch or a running ccc); no new failures on this branch.
  • ruff check and ruff format --check clean on the touched files; mypy --strict clean on them apart from the pre-existing yaml stubs note.
  • Used in anger: our index daemons run with this hook and a small httpx Voyage adapter, litellm absent from the environment.

🤖 Generated with Claude Code

Add an embedder hook beside the two built-in providers. With
`provider: custom`, `embedding.embedder` names a `"module.path:callable"`
factory that cocoindex-code imports and calls as
`factory(settings, indexing_params)`; the result must satisfy cocoindex's
`VectorSchemaProvider` and expose an async `embed()`. This mirrors the
`module.path:callable` form the chunker registry already uses.

Why: the stock cloud path routes every embedding through litellm, which
is a large dependency tree (it pulls aiohttp, boto3, openai, tiktoken and
some forty others) and the subject of two PyPI supply-chain compromises
in 2026. Users who talk to a single provider can now install a small
adapter of their own beside the tool (`uv tool install cocoindex-code
--with my-embedders`) and leave litellm out of the environment entirely.

- settings: `EmbeddingSettings.embedder`, validated in `__post_init__`
  (required with `custom`, rejected elsewhere), round-tripped by the
  YAML (de)serialisers, with a comment template for `ccc init` output.
- embedder_params: a custom embedder owns its kwargs, so
  `validate_params` passes them through.
- shared: `load_custom_embedder_factory()` with errors that name the
  spec for a malformed value, an import failure, a missing attribute or
  a non-callable; `create_embedder()` dispatches to it and checks the
  returned object against the contract.
- tests/test_custom_embedder.py: settings validation and round trip,
  params pass-through, loading a factory from a temporary module,
  contract rejection, every loader error, and an embed/schema call.
- EMBEDDINGS.md: a "Custom Embedder" section; CLAUDE.md: one line in
  the embedding flow notes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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