feat: add Ollama embedding provider + unify provider selection (AGE-1)#28
Merged
Conversation
Adds Ollama as a third embedding backend alongside fastembed (local) and OpenAI, so users get high-quality embeddings that run fully offline and free. - New `src/embeddings/ollama.rs` (`OllamaProvider`) talking to Ollama's `/api/embed`. Host from `OLLAMA_HOST` (default http://localhost:11434, bare host:port normalized), model from `OLLAMA_EMBED_MODEL` (default nomic-embed-text), optional `OLLAMA_API_KEY` bearer for remote hosts. Mirrors the openai.rs reqwest client + timeout + retry/backoff. Actionable errors when the daemon is down or the model isn't pulled. - Dynamic dimension: Ollama's dimension is model-dependent, so it is probed from the model on construction (sync `from_env`, plus `from_env_async` for the MCP async runtime) rather than a compile-time constant. - Provider-selection refactor: replaced the `use_openai: bool` duplicated across ~6 call sites with a `Provider` enum + single `build_provider` factory and a `--provider <local|openai|ollama>` flag on `embed`, `semantic`, `smart`, and `similar`, plus a `provider` field on the MCP smart_context tool. `--openai` is kept as a deprecated alias. - Provider/dimension mismatch against an existing index is detected and warned via a shared `warn_index_mismatch` (embeddings live in different vector spaces per provider/model, so switching requires re-embedding). - Fix a latent CLI bug surfaced by the new debug test: `ctx embed`'s `--force` claimed `-f`, which collides with the global `-f/--format`; dropped the short (other subcommands' `--force` are already long-only). This only asserted in debug builds, so no prior (release) invocation hit it. Tests + docs: - Unit tests for host/model resolution and `/api/embed` response parsing (success, dimension mismatch, model-not-found, empty). - `tests/ollama_smart_e2e.rs`: a real end-to-end `ctx smart --provider ollama` relevance test, gated behind `CTX_TEST_OLLAMA=1` + a reachability probe so it is a no-op in normal CI. Verified locally against a live daemon with qwen3-embedding:8b — `ctx smart` selects the on-topic file over off-topic ones. - Documented the providers + env vars in README and the docs site.
…rovider
Adds an optional, committed `.ctx/config.toml` so a project can set defaults
once instead of passing flags/env vars on every command. Currently it configures
the embedding backend:
[embedding]
provider = "ollama" # local | openai | ollama
model = "qwen3-embedding:8b" # Ollama/OpenAI model
# host = "http://localhost:11434"
- New `src/config.rs` (`CtxConfig`/`EmbeddingConfig`) loaded from
`<root>/.ctx/config.toml`; missing or malformed files fall back to defaults
(never fatal — config is optional), and unknown keys are ignored so older
binaries tolerate newer files.
- Resolution precedence is always CLI flag > env var > config > built-in
default, so the file never overrides an explicit request:
- provider: `Provider::resolve` gains the config default as the lowest-priority
source (wired in main.rs and the MCP tool);
- Ollama model/host: `OLLAMA_EMBED_MODEL`/`OLLAMA_HOST` still win over the
config's `model`/`host`, threaded through `build_provider` → `OllamaProvider::from_config`.
- `.gitignore`: `.ctx/` stays ignored but `.ctx/config.toml` is kept tracked so
the project default is shared with the team.
Tests: config parsing (present/absent/malformed/unknown-keys) and the resolution
precedence. Docs: documented `.ctx/config.toml` in the configuration guide.
Collaborator
Author
|
Added project config support: an optional committed |
saldestechnology
added a commit
that referenced
this pull request
Jul 11, 2026
Sets `.ctx/config.toml` so `ctx embed`/`semantic`/`smart`/`similar` use the local Ollama backend (qwen3-embedding:8b) by default here, without needing `--provider`/env vars. The index has been re-embedded with this model. Requires ctx built from #28 (Ollama provider + config support); older binaries ignore the file and fall back to the local fastembed default.
saldestechnology
added a commit
that referenced
this pull request
Jul 11, 2026
…atures The project-config load added in #28 was named `config`, shadowing the `SmartConfig config` above it, so `smart_context_with_embedding(..., config)` passed the wrong type. Only compiled under the `mcp` feature, so the default build (and macOS CI) missed it; `--all-features` failed to compile. Rename the project config to `project_config`. (Latent on main from #28; this fix lands there via this PR.)
saldestechnology
added a commit
that referenced
this pull request
Jul 11, 2026
…rt (#25) * feat: lexical path boost + oversized-top budget guarantee for ctx smart Two residual `ctx smart` misses remained after #18 (which made selection deterministic and semantic-first), each with a distinct cause: - A candidate whose path literally contains a task word could still be outranked by the semantic matches and squeezed out by the token budget (e.g. `embeddings/openai.rs` for "…openai" — present only via a call-graph edge). A lexical signal fixes this. - The single most-relevant file could exceed the whole budget and be dropped by greedy first-fit, which then backfilled with smaller, less-relevant files (e.g. `parser/solidity.rs`, 9074 tokens > the 8000 budget, was already rank 1 yet vanished). A budget-policy fix is needed. Part A — lexical path boost: - New shared `utils::lexical_tokens`: lowercases, splits on non-alphanumeric and camelCase boundaries, drops length-≤-1 tokens and a small stopword set. - `FileSelection` gains `lexical_score` = count of distinct task tokens that appear in the file's PATH (path only, not symbol names: matching identifiers would let ubiquitous names like `ctx` — a test helper across `tests/*_cli.rs` — score a hit on nearly every file and drown out the on-topic one). - `rank_files` treats a lexical hit as top-tier alongside semantic matches and orders lexical hits first within the tier — surfacing the on-topic file without any tunable float weight. Determinism (path tie-break) is preserved. Part B — budget guarantee: - `select_with_guaranteed_top` always includes the rank-1 file even when it alone exceeds the budget, then first-fits the remainder (reusing `select_by_token_budget`). The handler prints a one-line stderr note when the top file is oversized. Adds unit tests for the tokenizer, lexical promotion/ordering, path-only scoring (asserting `ctx` in a symbol name does not match), and the oversized- top budget policy. #18's determinism/tiering tests still pass with `lexical_score = 0`. Verified end-to-end: "…openai" now selects `embeddings/openai.rs` at rank 1; "parse solidity contracts" includes `parser/solidity.rs` with the oversize note; "add a new output format to ctx sql" keeps `src/commands/sql.rs` at rank 1 and stays deterministic across runs. * test: deterministic offline regression guard for ctx smart ranking Adds tests/smart_relevance.rs — a library-level integration test that guards the smart file-selection ranking against regressions without needing the fastembed model (which downloads ~90MB and is not byte-stable across arches, so it cannot run in CI). It hand-builds a tiny on-disk index (files + symbols + edges) and seeds exact embedding vectors via the library API (Database::store_embedding), then calls smart_context_with_embedding directly with a crafted query vector — exercising the full seed -> call-graph expand -> lexical -> rank -> budget pipeline deterministically and offline. Two scenarios: - path_match_beats_symbol_name_noise: a task token in a file's PATH must promote the on-topic file over a higher-semantic-scored file whose only match is a symbol NAME. This reproduces the `ctx` test-helper regression that was originally caught by hand. Verified as a real guard via mutation: reverting compute_lexical_score to also match symbol names makes this test fail. - graph_only_path_match_is_promoted: a candidate reachable only through call-graph expansion whose path matches the task is surfaced to the top rather than being outranked by the semantic matches and dropped. * fix: MCP analysis config variable shadowed SmartConfig under --all-features The project-config load added in #28 was named `config`, shadowing the `SmartConfig config` above it, so `smart_context_with_embedding(..., config)` passed the wrong type. Only compiled under the `mcp` feature, so the default build (and macOS CI) missed it; `--all-features` failed to compile. Rename the project config to `project_config`. (Latent on main from #28; this fix lands there via this PR.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements AGE-1. Adds Ollama as a third embedding backend (alongside fastembed/local and OpenAI), so users get high-quality embeddings that run fully offline and free — and makes real end-to-end
ctx smartrelevance testing viable (the fastembed model can't run deterministically in CI; a local Ollama can).What's new
OllamaProvider(src/embeddings/ollama.rs) → Ollama/api/embed.OLLAMA_HOST(defaulthttp://localhost:11434, barehost:portnormalized), modelOLLAMA_EMBED_MODEL(defaultnomic-embed-text), optionalOLLAMA_API_KEYbearer for remote hosts.openai.rs; actionable errors when the daemon is down or the model isn't pulled (ollama pull …).nomic-embed-text768,qwen3-embedding:8b4096, …), so it's probed from the model on construction (syncfrom_env, plusfrom_env_asyncfor the MCP runtime), not a compile-time constant.use_openai: boolduplicated across ~6 call sites with aProviderenum + onebuild_providerfactory and a--provider <local|openai|ollama>flag onembed,semantic,smart,similar, plus aproviderfield on the MCPsmart_contexttool.--openaikept as a deprecated alias.warn_index_mismatchwarns when the query provider/dimension differs from the index (different providers/models occupy different vector spaces → re-embed).Incidental fix
ctx embed's--forceclaimed-f, colliding with the global-f/--format. This only trips clap's debug-build assertion, so no prior release invocation hit it — but the new debug test surfaced it. Dropped the short (other subcommands'--forceare already long-only; the watch commands use--force).Tests + docs
/api/embedresponse parsing (success, dimension mismatch, model-not-found, empty).tests/ollama_smart_e2e.rs— a realctx smart --provider ollamarelevance test, gated behindCTX_TEST_OLLAMA=1+ a daemon reachability probe, so it's a no-op in normal CI (no charges, no downloads). Asserts relative ranking (not vectors) so it survives model drift. Verified locally against a live daemon withqwen3-embedding:8b:ctx smart "parse solidity contract source into a syntax tree"selectssrc/parser.rsabove the off-topic files.Verification
All gates green:
cargo fmt --check,cargo clippy --all-targets -D warnings, full suite (298 lib incl. new Ollama unit tests + all integration suites). The gated e2e passes live and skips cleanly whenCTX_TEST_OLLAMAis unset.Acceptance criteria (AGE-1)
OllamaProviderimplementsEmbeddingProviderwith configurable host + modelembed/similar/smart/ MCP analysis can all use--provider ollamause_openai)Run the e2e locally:
CTX_TEST_OLLAMA=1 cargo test --test ollama_smart_e2e -- --nocapture(setCTX_TEST_OLLAMA_MODELto override the defaultqwen3-embedding:8b).