fix(ai): trust user-declared embedding dims for local recipes + litellm /v1 hint (#2271, #2209)

#2271: a new trust_custom_dims flag adds a passthrough tier so ollama /
llama-server / litellm accept a user-supplied --embedding-dimensions instead of
being hard-rejected. Fail-closed for fixed-dim providers (openai/voyage/
zeroentropy) and excludes openrouter (declares dims_options). Register modern
ollama embed model names.

#2209: litellm setup_hint now states the /v1 path convention and the docs
pointer is corrected to docs/integrations/embedding-providers.md.
This commit is contained in:
Garry Tan
2026-07-02 08:02:24 -07:00
parent 36bf249c5e
commit 071731c591
6 changed files with 77 additions and 3 deletions
+3 -2
View File
@@ -6,7 +6,7 @@ import type { Recipe } from '../types.ts';
* gbrain at it via `LITELLM_BASE_URL`. The proxy normalizes to
* OpenAI-compatible API.
*
* See docs/guides/litellm-proxy.md for the setup recipe.
* See docs/integrations/embedding-providers.md for the setup recipe.
*/
export const litellmProxy: Recipe = {
id: 'litellm',
@@ -25,6 +25,7 @@ export const litellmProxy: Recipe = {
models: [],
user_provided_models: true, // v0.32 D8=A wire-through for the litellm hardcode
default_dims: 0, // user must declare --embedding-dimensions explicitly
trust_custom_dims: true, // #2271: proxy-backed model dim is user-declared
cost_per_1m_tokens_usd: undefined,
price_last_verified: '2026-04-20',
// LiteLLM's batch capacity is determined by the backend it proxies;
@@ -41,5 +42,5 @@ export const litellmProxy: Recipe = {
supports_multimodal: true,
},
},
setup_hint: 'Run LiteLLM (https://docs.litellm.ai) in front of any provider; set LITELLM_BASE_URL + pass --embedding-model litellm:<model> and --embedding-dimensions <N>.',
setup_hint: 'Run LiteLLM (https://docs.litellm.ai) in front of any provider; set LITELLM_BASE_URL (include the /v1 suffix if your proxy serves the OpenAI route there, e.g. http://localhost:4000/v1) + pass --embedding-model litellm:<model> and --embedding-dimensions <N>.',
};
+1
View File
@@ -32,6 +32,7 @@ export const llamaServer: Recipe = {
models: [], // user-driven; whatever model the server was launched with
user_provided_models: true,
default_dims: 0, // forces explicit --embedding-dimensions
trust_custom_dims: true, // #2271: user knows the launched model's native dim
cost_per_1m_tokens_usd: 0,
price_last_verified: '2026-05-10',
// llama-server's batch capacity is set by `--ctx-size` at launch
+13 -1
View File
@@ -13,8 +13,20 @@ export const ollama: Recipe = {
},
touchpoints: {
embedding: {
models: ['nomic-embed-text', 'mxbai-embed-large', 'all-minilm'],
// #2271: modern local embed models added so assertTouchpoint accepts them.
// Each carries its own native dim (qwen3-embed-8b=4096, arctic-l-v2=1024);
// the recipe-wide default_dims below is only the nomic fallback, so users
// of the larger models pass --embedding-dimensions (allowed via
// trust_custom_dims). Per-model dims metadata is a tracked follow-up.
models: [
'nomic-embed-text',
'mxbai-embed-large',
'all-minilm',
'qwen3-embed-8b',
'snowflake-arctic-embed-l-v2',
],
default_dims: 768, // nomic-embed-text native dim
trust_custom_dims: true, // #2271: local models carry varied native dims
cost_per_1m_tokens_usd: 0,
price_last_verified: '2026-04-20',
// Ollama's batch capacity depends on the locally loaded model + the
+11
View File
@@ -87,6 +87,17 @@ export interface EmbeddingTouchpoint {
* for shorthand `--model <provider>` and prints a setup hint.
*/
user_provided_models?: true;
/**
* #2271: trust a user-supplied `--embedding-dimensions` for this recipe even
* when it's not in the known-Matryoshka allowlist. Set ONLY on local /
* bring-your-own-backend recipes where the user knows their model's native dim
* and we can't enumerate every model (ollama, llama-server, litellm). The
* provider's `/embeddings` response-dim validation catches a genuine mismatch
* pre-storage. Must NOT be set on fixed-dim hosted providers (openai/voyage/
* zeroentropy stay fail-closed) or on recipes that declare recipe-wide
* `dims_options` (e.g. openrouter, whose Tier-1 options legitimately govern).
*/
trust_custom_dims?: true;
/**
* v0.32 (#779 reworked): explicit opt-out of the missing-max_batch_tokens
* startup warning. Set to `true` for recipes whose batch capacity is
+12
View File
@@ -451,6 +451,18 @@ function isCustomDimValidForProvider(
};
}
// Passthrough tier (#2271): local / bring-your-own-backend recipes (ollama,
// llama-server, litellm) flag trust_custom_dims because the user knows their
// model's native dim and we can't enumerate every locally-pulled model. Trust
// the requested dim; the provider's /embeddings response-dim validation catches
// a genuine mismatch pre-storage. Runs AFTER Tier 1 (recipe dims_options) and
// Tier 2 (provider Matryoshka allowlists) so a recipe that DOES declare fixed
// options (e.g. openrouter) is still governed by those, and fixed-dim hosted
// providers (openai/voyage/zeroentropy) never reach here as valid.
if (recipe.touchpoints.embedding?.trust_custom_dims === true) {
return { valid: true, error: '' };
}
// Tier 3: provider not known to support custom dims at all.
return {
valid: false,
+37
View File
@@ -257,6 +257,43 @@ describe('resolveSchemaEmbeddingDim', () => {
if (got.ok) expect(got.dim).toBe(768);
});
// #2271 — trust_custom_dims passthrough for local / BYO-backend recipes.
test('ollama accepts a custom dim for a modern model via trust_custom_dims', () => {
const got = resolveSchemaEmbeddingDim({
embedding_model: 'ollama:qwen3-embed-8b',
embedding_dimensions: 4096,
});
expect(got.ok).toBe(true);
if (got.ok) expect(got.dim).toBe(4096);
});
test('litellm accepts a user-declared custom dim via trust_custom_dims', () => {
const got = resolveSchemaEmbeddingDim({
embedding_model: 'litellm:bge-large',
embedding_dimensions: 1024,
});
expect(got.ok).toBe(true);
if (got.ok) expect(got.dim).toBe(1024);
});
test('llama-server accepts a user-declared custom dim via trust_custom_dims', () => {
const got = resolveSchemaEmbeddingDim({
embedding_model: 'llama-server:nomic-embed-text-v1.5',
embedding_dimensions: 2560,
});
expect(got.ok).toBe(true);
if (got.ok) expect(got.dim).toBe(2560);
});
test('[REGRESSION] openrouter (declares dims_options, NOT flagged) still fail-closed on an unlisted dim', () => {
const got = resolveSchemaEmbeddingDim({
embedding_model: 'openrouter:openai/text-embedding-3-small',
embedding_dimensions: 4096,
});
expect(got.ok).toBe(false);
if (!got.ok) expect(got.error).toMatch(/rejects custom dimensions 4096|does not support custom dimensions/);
});
test('unknown provider rejected with provider list hint', () => {
const got = resolveSchemaEmbeddingDim({ embedding_model: 'notarealprovider:foo' });
expect(got.ok).toBe(false);