Skip to content

Commit b9add45

Browse files
Merge pull request #61 from VioletCranberry/feat/embedding-base-url
feat: add `baseUrl` to embedding config for local OpenAI-compatible s…
2 parents 2626080 + e1ae715 commit b9add45

17 files changed

Lines changed: 235 additions & 22 deletions

.env.example

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,16 @@
3030
# COCOSEARCH_EMBEDDING_PROVIDER=ollama
3131

3232
# API key for remote embedding providers (OpenAI, OpenRouter)
33-
# Required when COCOSEARCH_EMBEDDING_PROVIDER is not "ollama"
33+
# Required when COCOSEARCH_EMBEDDING_PROVIDER is not "ollama" (unless baseUrl is set)
3434
# COCOSEARCH_EMBEDDING_API_KEY=sk-...
3535

36+
# Base URL for any embedding provider. Use this to point any provider at a local
37+
# OpenAI-API-compatible server (Infinity, text-embeddings-inference, vLLM, etc.)
38+
# instead of the default endpoint. When set for a remote provider, the API key
39+
# requirement is relaxed (local servers typically don't need one).
40+
# For the "ollama" provider, this overrides COCOSEARCH_OLLAMA_URL.
41+
# COCOSEARCH_EMBEDDING_BASE_URL=http://localhost:8080
42+
3643
# =============================================================================
3744
# Optional (default: auto-detected from cocosearch.yaml, git root, or cwd)
3845
# =============================================================================

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ uv run cocosearch mcp --project-from-cwd
111111
- **`indexer/flow.py`** — CocoIndex flow definition (the indexing pipeline)
112112
- **`search/`** — Hybrid search engine: RRF fusion of vector + keyword results, two-level LRU query cache (`cache.py` — exact + semantic similarity at cosine > 0.92), context expansion via Tree-sitter boundaries for 10 languages (`context_expander.py`, exports `CONTEXT_EXPANSION_LANGUAGES`), symbol/language filtering (`filters.py`), auto-detection of code identifiers for hybrid mode (`query_analyzer.py`), optional dependency enrichment (`include_deps` attaches direct dependencies/dependents to search results), interactive REPL (`repl.py`), result formatting (`formatter.py`), pipeline analysis with stage-by-stage diagnostics (`analyze.py`)
113113
- **`search/db.py`** — PostgreSQL connection pool (singleton) and query execution
114-
- **`config/`** — YAML config with 4-level precedence resolution (CLI > env > file > defaults), `${VAR}` substitution (`env_substitution.py`), Pydantic schema validation (`schema.py` with `extra="forbid"`, `strict=True`, `EmbeddingSection` with `provider` field and provider-aware model defaults, `LoggingSection` with `file` toggle), user-friendly error formatting with fuzzy field suggestions (`errors.py`), env var validation (`env_validation.py`)
114+
- **`config/`** — YAML config with 4-level precedence resolution (CLI > env > file > defaults), `${VAR}` substitution (`env_substitution.py`), Pydantic schema validation (`schema.py` with `extra="forbid"`, `strict=True`, `EmbeddingSection` with `provider` field, provider-aware model defaults, and optional `baseUrl` for custom endpoints, `LoggingSection` with `file` toggle), user-friendly error formatting with fuzzy field suggestions (`errors.py`), env var validation (`env_validation.py`)
115115
- **`management/`** — Index lifecycle: discovery (`discovery.py`), stats (`stats.py` — includes `check_deps_staleness()` for dependency freshness checks), clearing (`clear.py`), git-based naming (`git.py`), metadata with collision detection, status tracking, embedding provider/model tracking, and `deps_extracted_at` timestamp (`metadata.py`), project root detection (`context.py`)
116116
- **`deps/`** — Dependency graph framework: pluggable extractors (`extractors/`), pluggable module resolvers (`resolver.py`), edge storage (`db.py`), extraction orchestrator (`extractor.py`), query API with transitive BFS traversal (`query.py`), data models (`models.py`), autodiscovery registry (`registry.py`). 11 extractors: Python imports, JavaScript/TypeScript (ES6 + CommonJS + re-exports), Go imports, ArgoCD (Application/ApplicationSet/AppProject — project refs, source repos/charts/paths, destinations, generator repos; multi-document YAML via `safe_load_all`), Docker Compose (image/depends_on/extends), GitHub Actions (uses refs with parsed owner/repo/version, needs inter-job deps), GitLab CI (include local/project/remote/template, extends template inheritance, needs DAG deps, trigger child/multi-project pipelines, image/service refs), Terraform (module sources with version, required_providers, remote_state, tfvars associations), Helm (template includes, Chart.yaml subcharts, chart membership ownership with `is_subchart` indicator, subchart-to-parent links), Markdown (documentation references: frontmatter depends, links, inline code, code blocks). 5 module resolvers: Python (dotted modules, `__init__.py`, relative imports, `src/`/`lib/` prefix stripping), JavaScript (extension probing, index files), Go (import path suffix matching), Terraform (local module sources), Markdown (relative path normalization, directory reference matching). Query layer supports direct lookups (`get_dependencies`/`get_dependents`), transitive BFS trees (`get_dependency_tree`/`get_impact` with cycle detection and depth limits), batch-aware multi-root BFS (`get_dependency_tree_batch`/`get_impact_batch` with shared visited set), and detailed stats (`get_dep_stats_detailed`). Three edge types: "import" (code imports), "call" (symbol calls), "reference" (grammar-level refs with `metadata.kind` for specifics — Helm uses `chart_member` for template/values→Chart.yaml ownership and `subchart_of` for subchart→parent chart links).
117117
- **`handlers/`** — Language-specific chunking (HCL, Go Template, Dockerfile, Bash, Scala, Groovy) and grammar handlers (`handlers/grammars/` — ArgoCD, Helm Chart, Helm Template, Helm Values, GitHub Actions, GitLab CI, Docker Compose, Kubernetes, Terraform) with autodiscovery registry
@@ -187,7 +187,7 @@ Project config via `cocosearch.yaml` (no leading dot) in project root. The `inde
187187

188188
**Logging:** Log file output is disabled by default. Enable via `logging.file: true` in `cocosearch.yaml` or `COCOSEARCH_LOG_FILE=true` env var. Logs are written to `~/.cocosearch/logs/cocosearch.log` with 10MB rotation and 3 backups. The web dashboard log panel supports category filtering (search, index, mcp, cache, infra, system, deps) and level filtering (DEBUG+, INFO+, WARN+, ERROR+).
189189

190-
**Embedding providers:** CocoSearch supports multiple embedding providers: `ollama` (default, local), `openai`, and `openrouter`. Provider selection is via `COCOSEARCH_EMBEDDING_PROVIDER` env var or the `embedding.provider` field in `cocosearch.yaml`. Remote providers require `COCOSEARCH_EMBEDDING_API_KEY`. Default models: ollama→`nomic-embed-text`, openai→`text-embedding-3-small`, openrouter→`openai/text-embedding-3-small`. Index metadata tracks which provider/model was used; switching requires `--fresh` reindex.
190+
**Embedding providers:** CocoSearch supports multiple embedding providers: `ollama` (default, local), `openai`, and `openrouter`. Provider selection is via `COCOSEARCH_EMBEDDING_PROVIDER` env var or the `embedding.provider` field in `cocosearch.yaml`. Remote providers require `COCOSEARCH_EMBEDDING_API_KEY` (unless `baseUrl` is set for local OpenAI-compatible servers). `COCOSEARCH_EMBEDDING_BASE_URL` (or `embedding.baseUrl` in config) overrides the provider's default endpoint — use it with local OpenAI-API-compatible servers (Infinity, text-embeddings-inference, vLLM). For the `ollama` provider, `baseUrl` overrides `COCOSEARCH_OLLAMA_URL`. Default models: ollama→`nomic-embed-text`, openai→`text-embedding-3-small`, openrouter→`openai/text-embedding-3-small`. Index metadata tracks which provider/model was used; switching requires `--fresh` reindex.
191191

192192
**Docker / client mode env vars:**
193193
- `COCOSEARCH_SERVER_URL` — When set, CLI forwards commands to the remote server instead of running locally (e.g., `http://localhost:3000`)

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ indexing:
575575
embedding:
576576
provider: ollama # ollama (default), openai, openrouter
577577
model: nomic-embed-text # default depends on provider
578+
# baseUrl: http://localhost:8080 # custom OpenAI-compatible endpoint
578579
```
579580

580581
### Remote Embedding Providers
@@ -599,11 +600,24 @@ uv run cocosearch config check
599600
| Provider | Default Model | API Key Required |
600601
|----------|--------------|-----------------|
601602
| `ollama` | `nomic-embed-text` | No (local) |
602-
| `openai` | `text-embedding-3-small` | Yes |
603-
| `openrouter` | `openai/text-embedding-3-small` | Yes |
603+
| `openai` | `text-embedding-3-small` | Yes (optional with `baseUrl`) |
604+
| `openrouter` | `openai/text-embedding-3-small` | Yes (optional with `baseUrl`) |
604605

605606
Switching providers on an existing index requires `--fresh` to reindex with the new embedding model.
606607

608+
#### Custom Endpoints
609+
610+
Use `embedding.baseUrl` (or `COCOSEARCH_EMBEDDING_BASE_URL`) to point any provider at a local OpenAI-compatible server such as [Infinity](https://github.com/michaelfeil/infinity), [text-embeddings-inference](https://github.com/huggingface/text-embeddings-inference), or [vLLM](https://github.com/vllm-project/vllm):
611+
612+
```yaml
613+
embedding:
614+
provider: openai
615+
model: BAAI/bge-small-en-v1.5
616+
baseUrl: http://localhost:8080
617+
```
618+
619+
When `baseUrl` is set, the API key is not required. For the `ollama` provider, `baseUrl` overrides `COCOSEARCH_OLLAMA_URL`.
620+
607621
## Testing
608622

609623
Tests use [pytest](https://docs.pytest.org/). All tests are unit tests, fully mocked, and require no infrastructure. Markers are auto-applied based on directory -- no need to add them manually.

docs/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ CocoSearch is a local-first hybrid semantic code search system. This document pr
1212

1313
## System Components
1414

15-
**Embedding Provider:** Generates 768-dimensional vectors from code chunks. By default, Ollama runs `nomic-embed-text` locally — no API keys, no network calls. Optional remote providers (OpenAI with `text-embedding-3-small`, OpenRouter) are available for teams that prefer managed infrastructure. Implementation: `src/cocosearch/indexer/embedder.py`
15+
**Embedding Provider:** Generates 768-dimensional vectors from code chunks. By default, Ollama runs `nomic-embed-text` locally — no API keys, no network calls. Optional remote providers (OpenAI with `text-embedding-3-small`, OpenRouter) are available for teams that prefer managed infrastructure. Any provider can be pointed at a custom endpoint via `baseUrl` for local OpenAI-compatible servers (Infinity, text-embeddings-inference, vLLM). Implementation: `src/cocosearch/indexer/embedder.py`
1616

1717
**PostgreSQL + pgvector:** Database storing code chunks with their vector embeddings. The pgvector extension enables efficient cosine similarity search over embedding vectors. Also provides full-text search via tsvector columns for keyword matching. Implementation: `src/cocosearch/search/db.py`
1818

@@ -90,7 +90,7 @@ See [MCP Tools Reference](mcp-tools.md) for complete parameter documentation, re
9090

9191
## Key Design Decisions
9292

93-
**Local-first:** All processing happens on your machine by default. Ollama runs the embedding model locally, PostgreSQL stores data locally. Optional remote embedding providers (OpenAI, OpenRouter) send only chunk text for embedding — all indexing, storage, and search remain local. Your code never leaves your environment.
93+
**Local-first:** All processing happens on your machine by default. Ollama runs the embedding model locally, PostgreSQL stores data locally. Optional remote embedding providers (OpenAI, OpenRouter) send only chunk text for embedding — all indexing, storage, and search remain local. Any provider can also target a local OpenAI-compatible server via `baseUrl`, keeping embeddings fully on-machine without Ollama. Your code never leaves your environment.
9494

9595
**Infra-only Docker:** Docker provides PostgreSQL+pgvector and Ollama infrastructure only. CocoSearch runs natively via `uvx` for faster iteration and simpler updates. This keeps the Docker image lightweight and avoids Python dependency management inside containers.
9696

docs/how-it-works.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ By default, everything described above runs on your machine:
134134

135135
The only external dependencies are Docker (to run Postgres and Ollama) and the embedding model weights (downloaded once by Ollama). After that, you could run CocoSearch on an airplane.
136136

137-
**Optional remote embeddings:** If you prefer managed infrastructure or don't want to run Ollama locally, CocoSearch supports OpenAI and OpenRouter as embedding providers. When using a remote provider, only chunk text is sent for embedding — all indexing logic, storage, and search remain fully local. Configure via `embedding.provider` in `cocosearch.yaml` or the `COCOSEARCH_EMBEDDING_PROVIDER` environment variable.
137+
**Optional remote embeddings:** If you prefer managed infrastructure or don't want to run Ollama locally, CocoSearch supports OpenAI and OpenRouter as embedding providers. When using a remote provider, only chunk text is sent for embedding — all indexing logic, storage, and search remain fully local. Configure via `embedding.provider` in `cocosearch.yaml` or the `COCOSEARCH_EMBEDDING_PROVIDER` environment variable. You can also use `embedding.baseUrl` (or `COCOSEARCH_EMBEDDING_BASE_URL`) to point any provider at a local OpenAI-compatible server (Infinity, text-embeddings-inference, vLLM) — in that case, no API key is required and embeddings stay fully local.
138138

139139
## Beyond Search: Dependency Graph
140140

docs/mcp-configuration.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,14 @@ claude mcp add --scope user \
197197
--env COCOSEARCH_EMBEDDING_API_KEY=sk-... \
198198
cocosearch -- \
199199
uvx --from cocosearch cocosearch mcp --project-from-cwd
200+
201+
# Or with a local OpenAI-compatible server (no API key needed):
202+
claude mcp add --scope user \
203+
--env COCOSEARCH_EMBEDDING_PROVIDER=openai \
204+
--env COCOSEARCH_EMBEDDING_BASE_URL=http://localhost:8080 \
205+
--env COCOSEARCH_EMBEDDING_MODEL=BAAI/bge-small-en-v1.5 \
206+
cocosearch -- \
207+
uvx --from cocosearch cocosearch mcp --project-from-cwd
200208
```
201209

202210
**Claude Desktop / OpenCode (JSON config):**
@@ -212,7 +220,19 @@ Add to your server's `"env"` block (or `"environment"` for OpenCode):
212220
}
213221
```
214222

215-
Supported providers: `ollama` (default), `openai`, `openrouter`. With a remote provider, you do not need Ollama running — only PostgreSQL is required.
223+
For a local OpenAI-compatible server, use `COCOSEARCH_EMBEDDING_BASE_URL` instead of an API key:
224+
225+
```json
226+
{
227+
"env": {
228+
"COCOSEARCH_EMBEDDING_PROVIDER": "openai",
229+
"COCOSEARCH_EMBEDDING_BASE_URL": "http://localhost:8080",
230+
"COCOSEARCH_EMBEDDING_MODEL": "BAAI/bge-small-en-v1.5"
231+
}
232+
}
233+
```
234+
235+
Supported providers: `ollama` (default), `openai`, `openrouter`. With a remote provider, you do not need Ollama running — only PostgreSQL is required. Use `COCOSEARCH_EMBEDDING_BASE_URL` (or `embedding.baseUrl` in config) to point any provider at a custom endpoint.
216236

217237
### Project Detection
218238

docs/retrieval.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ The indexing pipeline transforms raw code files into searchable chunks with embe
7373
- Uses CocoIndex's shared transform — embedding function evaluated once and reused across all chunks in the flow
7474
- The filename prefix is only used for embedding input — the stored `content_text` column retains the raw chunk text
7575
- Same embedding function used during search queries to ensure consistency (search queries are NOT prefixed with filenames — intentional asymmetry: document embeddings are enriched, queries stay natural)
76-
- Ollama server address configured via `COCOSEARCH_OLLAMA_URL` environment variable (defaults to http://localhost:11434)
76+
- Server address configured via `COCOSEARCH_EMBEDDING_BASE_URL` (or `embedding.baseUrl` in config) for any provider, overriding the default endpoint. For the `ollama` provider specifically, `COCOSEARCH_OLLAMA_URL` is the fallback (defaults to http://localhost:11434)
7777

7878
**Implementation:** `src/cocosearch/indexer/embedder.py``add_filename_context`, `code_to_embedding`
7979

src/cocosearch/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1873,6 +1873,14 @@ def _source_label(source: str) -> str:
18731873
)
18741874
table.add_row("COCOSEARCH_OLLAMA_URL", ollama_url, ollama_url_source)
18751875

1876+
# EMBEDDING_BASE_URL (optional, any provider)
1877+
base_url, base_url_source = check_resolver.resolve(
1878+
"embedding.baseUrl", None, "COCOSEARCH_EMBEDDING_BASE_URL"
1879+
)
1880+
if base_url is not None:
1881+
base_url_source = _source_label(base_url_source)
1882+
table.add_row("COCOSEARCH_EMBEDDING_BASE_URL", base_url, base_url_source)
1883+
18761884
console.print(table)
18771885
console.print()
18781886

src/cocosearch/config/resolver.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ def _get_default_value(self, field_path: str) -> Any:
253253
def bridge_embedding_config(self) -> tuple[str, str]:
254254
"""Resolve embedding config and bridge to env vars.
255255
256-
Ensures COCOSEARCH_EMBEDDING_PROVIDER, COCOSEARCH_EMBEDDING_MODEL, and
257-
COCOSEARCH_EMBEDDING_OUTPUT_DIMENSION env vars reflect the full
258-
precedence chain (env > config file > default).
256+
Ensures COCOSEARCH_EMBEDDING_PROVIDER, COCOSEARCH_EMBEDDING_MODEL,
257+
COCOSEARCH_EMBEDDING_OUTPUT_DIMENSION, and COCOSEARCH_EMBEDDING_BASE_URL
258+
env vars reflect the full precedence chain (env > config file > default).
259259
260260
Returns:
261261
Tuple of (provider, model).
@@ -268,10 +268,16 @@ def bridge_embedding_config(self) -> tuple[str, str]:
268268
"embedding.outputDimension", None, "COCOSEARCH_EMBEDDING_OUTPUT_DIMENSION"
269269
)
270270

271+
base_url, _ = self.resolve(
272+
"embedding.baseUrl", None, "COCOSEARCH_EMBEDDING_BASE_URL"
273+
)
274+
271275
os.environ["COCOSEARCH_EMBEDDING_PROVIDER"] = str(provider)
272276
os.environ["COCOSEARCH_EMBEDDING_MODEL"] = str(model)
273277
if dim is not None:
274278
os.environ["COCOSEARCH_EMBEDDING_OUTPUT_DIMENSION"] = str(dim)
279+
if base_url is not None:
280+
os.environ["COCOSEARCH_EMBEDDING_BASE_URL"] = str(base_url)
275281

276282
return provider, model
277283

src/cocosearch/config/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class EmbeddingSection(BaseModel):
5050
provider: str = Field(default="ollama")
5151
model: str | None = Field(default=None)
5252
outputDimension: int | None = Field(default=None)
53+
baseUrl: str | None = Field(default=None)
5354

5455
@model_validator(mode="after")
5556
def _validate_provider_and_defaults(self) -> "EmbeddingSection":

0 commit comments

Comments
 (0)