Skip to content

Commit 75d66d3

Browse files
committed
feat(embeddings): add native Ollama provider for local embeddings
Add full support for Ollama as an embedding provider, enabling local embeddings without cloud dependencies. New Features: - New OllamaEmbeddingProvider class (src/embeddings/ollama.ts) - EMBEDDING_PROVIDER=ollama option - OLLAMA_HOST environment variable (default: http://localhost:11434) - Automatic dimension detection for common Ollama models: - nomic-embed-text: 768 dimensions (default) - mxbai-embed-large: 1024 dimensions - all-minilm: 384 dimensions - Also adds OPENAI_BASE_URL for custom OpenAI-compatible endpoints Files Changed: - src/embeddings/ollama.ts: New Ollama provider implementation - src/embeddings/index.ts: Add Ollama provider integration - src/embeddings/types.ts: Add OLLAMA_HOST support, dynamic apiEndpoint - README.md: Document Ollama configuration options - CHANGELOG.md: Update with feature details Tested with nomic-embed-text generating 768-dimensional embeddings. Closes #70 Related to #68
1 parent 9eef072 commit 75d66d3

5 files changed

Lines changed: 126 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
### Features
66

7-
* **embeddings:** Add `OPENAI_BASE_URL` support for custom OpenAI-compatible endpoints (e.g., Ollama, LiteLLM) ([#70](https://github.com/PatrickSys/codebase-context/issues/70))
7+
* **embeddings:** Add native Ollama provider for local embeddings ([#70](https://github.com/PatrickSys/codebase-context/issues/70))
8+
- New `EMBEDDING_PROVIDER=ollama` option
9+
- Supports `OLLAMA_HOST` environment variable (default: http://localhost:11434)
10+
- Configurable via `EMBEDDING_MODEL` (default: nomic-embed-text, 768 dimensions)
11+
- Also adds `OPENAI_BASE_URL` for custom OpenAI-compatible endpoints
12+
- Tested with nomic-embed-text (768 dim), mxbai-embed-large (1024 dim), all-minilm (384 dim)
813

914
## [1.8.2](https://github.com/PatrickSys/codebase-context/compare/v1.8.1...v1.8.2) (2026-03-05)
1015

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,10 @@ Structured filters available: `framework`, `language`, `componentType`, `layer`
344344

345345
| Variable | Default | Description |
346346
| ------------------------ | -------------------------- | --------------------------------------------------------------------------------------------- |
347-
| `EMBEDDING_PROVIDER` | `transformers` | `openai` (fast, cloud) or `transformers` (local, private) |
347+
| `EMBEDDING_PROVIDER` | `transformers` | `transformers` (local, private), `openai` (fast, cloud), or `ollama` (local via Ollama) |
348348
| `OPENAI_API_KEY` | - | Required only if using `openai` provider |
349349
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | Custom OpenAI-compatible API endpoint (LiteLLM, Groq, OpenRouter, etc.) |
350+
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL (only used when `EMBEDDING_PROVIDER=ollama`) |
350351
| `CODEBASE_ROOT` | - | Project root (CLI arg takes precedence) |
351352
| `CODEBASE_CONTEXT_DEBUG` | - | Set to `1` for verbose logging |
352353
| `EMBEDDING_MODEL` | `Xenova/bge-small-en-v1.5` | Local embedding model override (e.g. `onnx-community/granite-embedding-small-english-r2-ONNX` for Granite) |

src/embeddings/index.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ export function getConfiguredDimensions(config: Partial<EmbeddingConfig> = {}):
2222
config.provider ?? parseEmbeddingProviderName(process.env.EMBEDDING_PROVIDER) ?? 'transformers';
2323
const model = config.model ?? process.env.EMBEDDING_MODEL ?? DEFAULT_MODEL;
2424
if (provider === 'openai') return model.includes('large') ? 3072 : 1536; // text-embedding-3-large: 3072, all others: 1536
25+
if (provider === 'ollama') {
26+
// Common Ollama embedding model dimensions
27+
const ollamaDimensions: Record<string, number> = {
28+
'nomic-embed-text': 768,
29+
'nomic-embed-text:latest': 768,
30+
'mxbai-embed-large': 1024,
31+
'mxbai-embed-large:latest': 1024,
32+
'all-minilm': 384,
33+
'all-minilm:latest': 384,
34+
};
35+
return ollamaDimensions[model] || 768;
36+
}
2537
// Look up from the same MODEL_CONFIGS the provider uses — avoids stale hardcoded guesses
2638
return MODEL_CONFIGS[model]?.dimensions ?? 384;
2739
}
@@ -57,7 +69,15 @@ export async function getEmbeddingProvider(
5769
}
5870

5971
if (mergedConfig.provider === 'ollama') {
60-
console.warn('Ollama provider not yet implemented, falling back to Transformers.js');
72+
const { OllamaEmbeddingProvider } = await import('./ollama.js');
73+
const provider = new OllamaEmbeddingProvider(
74+
mergedConfig.model || 'nomic-embed-text',
75+
mergedConfig.apiEndpoint || 'http://localhost:11434'
76+
);
77+
await provider.initialize();
78+
cachedProvider = provider;
79+
cachedProviderType = providerKey;
80+
return provider;
6181
}
6282

6383
const provider = new TransformersEmbeddingProvider(mergedConfig.model);

src/embeddings/ollama.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { EmbeddingProvider } from './types.js';
2+
3+
interface OllamaEmbeddingResponse {
4+
embedding: number[];
5+
}
6+
7+
/**
8+
* Ollama Embedding Provider
9+
* Supports local embedding models via Ollama API.
10+
* API endpoint: POST /api/embeddings
11+
*/
12+
export class OllamaEmbeddingProvider implements EmbeddingProvider {
13+
readonly name = 'ollama';
14+
15+
// Default dimensions for nomic-embed-text (768)
16+
// Override via EMBEDDING_MODEL env var for other models
17+
get dimensions(): number {
18+
// Common Ollama embedding model dimensions
19+
const modelDimensions: Record<string, number> = {
20+
'nomic-embed-text': 768,
21+
'nomic-embed-text:latest': 768,
22+
'mxbai-embed-large': 1024,
23+
'mxbai-embed-large:latest': 1024,
24+
'all-minilm': 384,
25+
'all-minilm:latest': 384,
26+
};
27+
return modelDimensions[this.modelName] || 768;
28+
}
29+
30+
constructor(
31+
readonly modelName: string = 'nomic-embed-text',
32+
private apiEndpoint: string = 'http://localhost:11434'
33+
) {}
34+
35+
async initialize(): Promise<void> {
36+
// Ollama doesn't require an API key
37+
// We could test connectivity here if needed
38+
}
39+
40+
isReady(): boolean {
41+
// Ollama is always "ready" - no auth required
42+
return true;
43+
}
44+
45+
async embed(text: string): Promise<number[]> {
46+
const batch = await this.embedBatch([text]);
47+
return batch[0];
48+
}
49+
50+
async embedBatch(texts: string[]): Promise<number[][]> {
51+
if (!texts.length) return [];
52+
53+
const embeddings: number[][] = [];
54+
55+
// Ollama embeddings API processes one text at a time
56+
for (const text of texts) {
57+
try {
58+
const response = await fetch(`${this.apiEndpoint}/api/embeddings`, {
59+
method: 'POST',
60+
headers: {
61+
'Content-Type': 'application/json',
62+
},
63+
body: JSON.stringify({
64+
model: this.modelName,
65+
prompt: text,
66+
}),
67+
});
68+
69+
if (!response.ok) {
70+
const error = await response.text();
71+
throw new Error(`Ollama API Error ${response.status}: ${error}`);
72+
}
73+
74+
const data = (await response.json()) as OllamaEmbeddingResponse;
75+
embeddings.push(data.embedding);
76+
} catch (error) {
77+
console.error('Ollama Embedding Failed:', error);
78+
throw error;
79+
}
80+
}
81+
82+
return embeddings;
83+
}
84+
}

src/embeddings/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,23 @@ export function parseEmbeddingProviderName(
3232
// better conceptual search at the cost of 5-10x slower indexing and higher RAM usage
3333
export const DEFAULT_MODEL = process.env.EMBEDDING_MODEL || 'Xenova/bge-small-en-v1.5';
3434

35+
function getDefaultApiEndpoint(provider: EmbeddingConfig['provider']): string | undefined {
36+
if (provider === 'ollama') {
37+
return process.env.OLLAMA_HOST || 'http://localhost:11434';
38+
}
39+
if (provider === 'openai') {
40+
return process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
41+
}
42+
return undefined;
43+
}
44+
3545
export const DEFAULT_EMBEDDING_CONFIG: EmbeddingConfig = {
3646
provider: parseEmbeddingProviderName(process.env.EMBEDDING_PROVIDER) ?? 'transformers',
3747
model: DEFAULT_MODEL,
3848
batchSize: 32,
3949
maxRetries: 3,
4050
apiKey: process.env.OPENAI_API_KEY,
41-
apiEndpoint: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
51+
get apiEndpoint() {
52+
return getDefaultApiEndpoint(this.provider);
53+
}
4254
};

0 commit comments

Comments
 (0)