Files
gbrain/test/embedding-model-dims.test.ts
T
Harrison BoothandGitHub 9690140bf3 fix(embeddings): resolve embedding dims per model, not per provider (#2051) (#3413)
The ollama recipe declared a single `default_dims: 768` (nomic-embed-text's
width) while serving models spanning 384..4096. Every non-nomic model
resolved to 768, so `gbrain init --embedding-model ollama:bge-m3` built a
768-wide `content_chunks.embedding` column for a model that emits 1024. The
schema looked fine and only failed at first insert with
`expected 768 dimensions, not 1024`.

Adds an optional `model_dims` map to `EmbeddingTouchpoint` and an
`embeddingDimsForModel()` resolver that prefers the per-model entry and falls
back to `default_dims`. The ollama recipe declares real widths for the models
it lists; bge-m3 is added to that list. The three `init` call sites that read
`default_dims` now resolve per model.

Partial by design: unlisted models still fall back to `default_dims`, and
`trust_custom_dims` keeps an explicit `--embedding-dimensions` override
working. `user_provided_models` recipes (litellm, llama-server) still resolve
to 0, so they continue to require explicit dimensions.

Verified end to end against an OpenAI-compatible stub standing in for Ollama,
using an isolated GBRAIN_HOME:

  before: config 768, content_chunks.embedding vector(768), insert fails
  after:  config 1024, content_chunks.embedding vector(1024), insert succeeds
2026-07-27 14:12:38 -07:00

69 lines
2.6 KiB
TypeScript

/**
* #2051 — per-model embedding dimensions for local recipes.
*
* Ollama serves models spanning 384..4096 dims, but the recipe declared a
* single `default_dims: 768` (nomic-embed-text's width). Every other model
* resolved to 768, so `gbrain init --embedding-model ollama:bge-m3` created a
* 768-wide column for a model emitting 1024 and the mismatch only surfaced at
* first insert.
*
* `embeddingDimsForModel()` consults the recipe's `model_dims` map first and
* falls back to `default_dims`, so:
* 1. Known models resolve to their true native width.
* 2. Unlisted models still fall back (no regression for arbitrary pulls).
* 3. `user_provided_models` recipes keep returning 0, which is what forces
* an explicit `--embedding-dimensions`.
*/
import { test, expect, describe } from 'bun:test';
import { getRecipe } from '../src/core/ai/recipes/index.ts';
import { embeddingDimsForModel } from '../src/core/ai/model-resolver.ts';
describe('embeddingDimsForModel — per-model dims (#2051)', () => {
const ollama = getRecipe('ollama')!;
test('bge-m3 resolves to its native 1024, not the recipe default 768', () => {
expect(embeddingDimsForModel(ollama, 'bge-m3')).toBe(1024);
});
test('accepts a provider-qualified id', () => {
expect(embeddingDimsForModel(ollama, 'ollama:bge-m3')).toBe(1024);
});
test.each([
['nomic-embed-text', 768],
['mxbai-embed-large', 1024],
['all-minilm', 384],
['qwen3-embed-8b', 4096],
['snowflake-arctic-embed-l-v2', 1024],
])('%s resolves to %i', (model, dims) => {
expect(embeddingDimsForModel(ollama, model as string)).toBe(dims as number);
});
test('every declared model_dims entry is also a listed model', () => {
const listed = new Set(ollama.touchpoints.embedding!.models);
for (const model of Object.keys(ollama.touchpoints.embedding!.model_dims ?? {})) {
expect(listed.has(model)).toBe(true);
}
});
test('an unlisted model falls back to the recipe default', () => {
expect(embeddingDimsForModel(ollama, 'some-model-pulled-locally')).toBe(768);
});
test('a missing model id falls back to the recipe default', () => {
expect(embeddingDimsForModel(ollama, undefined)).toBe(768);
});
test('llama-server still returns 0 so explicit dimensions stay required', () => {
const llamaServer = getRecipe('llama-server')!;
expect(embeddingDimsForModel(llamaServer, 'anything')).toBe(0);
});
test('fixed-dim hosted recipes are unaffected', () => {
const openai = getRecipe('openai')!;
const tp = openai.touchpoints.embedding!;
expect(embeddingDimsForModel(openai, tp.models[0])).toBe(tp.default_dims);
});
});