Skip to content

Commit 73ca382

Browse files
committed
fix(embeddings): correct width sizing, Ollama width resolution, and family gating
Round of review findings: - Size the shared indexing batch for the widest storable width; the aggregate guard rejects rather than splits, so a 3,072-wide base past 1,064 chunks failed - Resolve an unstated Ollama width from the server instead of defaulting to 1,536 - Substitute EMBEDDING_OUTPUT_DIMS alongside the model when evaluating the knowledge-embedding capability, so the chain is judged for the target at hand - Classify the embedding family exactly as the runtime does, so an id the runtime rejects cannot report its family as configured - Validate a capability field against the provider being configured, not the first one declaring the key, which rejected 384 in the Ollama wizard branch - Give OpenRouter the OpenAI-family model and width fields it was missing - Resolve model records by own property, so KB_EMBEDDING_MODEL=toString falls back - Narrow a knowledge base's width only for query searches, not tag-only ones - Report an unreachable Ollama as 502 rather than a missing model - Correct the sim-setup command and scope, the Ollama filtering claims, and the unstorable-width troubleshooting advice in the docs
1 parent ce85623 commit 73ca382

23 files changed

Lines changed: 316 additions & 85 deletions

File tree

apps/docs/content/docs/integrations/embeddings.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ Models differ in what they are good at and what they cost. `text-embedding-3-sma
1919

2020
Two things worth knowing before you build on it. Vectors are only comparable when they come from the same model at the same size, so changing either means re-embedding everything you intend to compare. And input longer than the model's limit is shortened to fit rather than rejected, with a warning in the run, so chunk long documents yourself when the tail matters.
2121

22-
Ollama is the exception to most of the above. It runs on your own deployment, so it needs no API key and costs nothing, and the model list is whatever you have pulled onto that server rather than a catalog Sim maintains — the block reads it live, filters it to models that can actually embed, and shows each one's vector width next to its name. Ollama accepts neither a task type nor a size reduction, so the block does not offer those controls for it. Self-hosted deployments configure the server with `OLLAMA_URL`; on Sim Cloud there is no Ollama to reach, so the list comes back empty.
22+
Ollama is the exception to most of the above. It runs on your own deployment, so it needs no API key and costs nothing, and the model list is whatever you have pulled onto that server rather than a catalog Sim maintains — the block reads it live, drops the models that report a non-embedding capability, and shows each one's vector width next to its name where Ollama reports one. A server too old to report either will list its chat models too and label none of them, so check the model you pick. Ollama accepts neither a task type nor a size reduction, so the block does not offer those controls for it. Self-hosted deployments configure the server with `OLLAMA_URL`; on Sim Cloud there is no Ollama to reach, so the list comes back empty.
2323

2424
Sim's knowledge bases embed separately: a base fixes one model and one vector width when it is created, from a smaller set of models. This block is for embedding text yourself inside a workflow.
2525
{/* MANUAL-CONTENT-END */}
2626

2727

2828
## Usage Instructions
2929

30-
Turn text into embedding vectors for semantic search, clustering, and similarity. Supports OpenAI, OpenRouter, Google Gemini, Cohere, and Mistral embedding models, plus any model on a self-hosted Ollama.
30+
Turn text into embedding vectors for semantic search, clustering, and similarity. Supports OpenAI, OpenRouter, Google Gemini, Cohere, and Mistral embedding models, plus embedding models on a self-hosted Ollama.
3131

3232

3333

apps/docs/content/docs/platform/self-hosting/environment-variables.mdx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,16 @@ document rather than a rejected configuration. Common pairings:
211211
| `1536` | `text-embedding-3-small`, `text-embedding-3-large`, `gemini-embedding-001` |
212212
| `3072` | `text-embedding-3-large`, `gemini-embedding-001` |
213213

214-
`sim-setup add knowledge-embeddings` walks through all of this — pick OpenAI, Azure OpenAI, Gemini,
215-
or Ollama and it writes the variables that family needs. `sim-setup status` then reports the one
216-
family your `KB_EMBEDDING_MODEL` actually selects, rather than every provider you happen to hold a
217-
key for.
218-
219-
The Embeddings block reads the same `OLLAMA_URL`. It lists the embedding-capable models installed on
220-
that server with the width each one emits, so a workflow can embed locally without an API key.
214+
On a Compose install or source checkout, `sim-setup add knowledge-embeddings` walks through all of
215+
this — pick OpenAI, Azure OpenAI, Gemini, or Ollama and it writes the variables that family needs.
216+
`sim-setup config` then reports the one family your `KB_EMBEDDING_MODEL` actually selects, rather
217+
than every provider you happen to hold a key for. Helm releases set these values through your own
218+
chart values instead, and an Ollama server is yours to run either way — `sim-setup` configures Sim
219+
to reach one, never installs it.
220+
221+
The Embeddings block reads the same `OLLAMA_URL`. It lists the models on that server that report an
222+
embedding capability, with the width each one emits where Ollama reports it, so a workflow can embed
223+
locally without an API key.
221224

222225
## Chat & PII
223226

apps/docs/content/docs/platform/self-hosting/troubleshooting.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ The load balancer's backend timeout is closing them. On GKE, attach a `BackendCo
252252

253253
Embeddings need a provider — set `OPENAI_API_KEY`, configure Azure OpenAI, set `KB_EMBEDDING_MODEL=gemini-embedding-001` with a Gemini key, or set `KB_EMBEDDING_MODEL=ollama/<model>` with `OLLAMA_URL` to embed on your own Ollama. If one is configured, verify pgvector is installed on the database.
254254

255-
A document that fails with `vector 0 has N unexpected dimensions` means `EMBEDDING_OUTPUT_DIMS` does not match what the model actually emits. Sim cannot check this for you — the message names both widths, so set the variable to the one the model returned and recreate the knowledge base. Existing knowledge bases keep the width they were created with.
255+
A document that fails with `vector 0 has N unexpected dimensions` means `EMBEDDING_OUTPUT_DIMS` does not match what the model actually emits. The message names both widths. If the width the model returned is one of `384`, `768`, `1024`, `1536`, or `3072`, set the variable to it and recreate the knowledge base. If it is anything else, no column can store it — choose a model that emits one of those five instead, since setting an unstorable width silently falls back to `1536` and the next document fails the same way. Existing knowledge bases keep the width they were created with.
256256

257257
## Credentials Unreadable After a Restore
258258

apps/sim/app/api/v1/knowledge/search/route.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
192192
* One query embedding serves every base in the request, so all of them must
193193
* be indexed the same way — including the vector width, which selects the
194194
* pgvector column each comparison reads.
195+
*
196+
* Built only for a query search. A tag-only request never embeds anything,
197+
* so resolving a width it will not use would let one base recorded at an
198+
* unstorable width fail a request that does not depend on it.
195199
*/
196-
const embeddingTargets = new Map<string, KbEmbeddingTarget>(
200+
const embeddingTargets = new Map(
197201
accessibleKbs.map((kb) => [
198202
`${kb.embeddingModel}:${kb.embeddingDimension}`,
199-
{ model: kb.embeddingModel, dimensions: toKbEmbeddingDimensions(kb.embeddingDimension) },
203+
{ model: kb.embeddingModel, dimensions: kb.embeddingDimension },
200204
])
201205
)
202206
if (hasQuery && embeddingTargets.size > 1) {
@@ -208,8 +212,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
208212
{ status: 400 }
209213
)
210214
}
211-
const queryEmbeddingTarget = [...embeddingTargets.values()][0]
212-
const queryEmbeddingModel = queryEmbeddingTarget.model
215+
const selectedTarget = [...embeddingTargets.values()][0]
216+
const queryEmbeddingModel = selectedTarget.model
217+
/**
218+
* The width is narrowed to a storable one only for a query search, which is
219+
* the only kind that reads a vector column. A tag-only search must not fail
220+
* on a width it never uses.
221+
*/
222+
const queryEmbeddingTarget: KbEmbeddingTarget | undefined = hasQuery
223+
? {
224+
model: selectedTarget.model,
225+
dimensions: toKbEmbeddingDimensions(selectedTarget.dimensions),
226+
}
227+
: undefined
213228

214229
let results: SearchResult[]
215230
let queryEmbeddingIsBYOK: boolean | null = null
@@ -235,7 +250,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
235250
} else if (hasQuery) {
236251
const queryEmbeddingResult = await generateSearchEmbedding(
237252
query!,
238-
queryEmbeddingTarget,
253+
queryEmbeddingTarget!,
239254
workspaceId
240255
)
241256
queryEmbeddingIsBYOK = queryEmbeddingResult.isBYOK
@@ -248,7 +263,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
248263
query,
249264
queryVector: {
250265
vector: JSON.stringify(queryEmbeddingResult.embedding),
251-
dimensions: queryEmbeddingTarget.dimensions,
266+
dimensions: queryEmbeddingTarget!.dimensions,
252267
},
253268
structuredFilters: hasFilters ? structuredFilters : undefined,
254269
})

apps/sim/blocks/blocks/embeddings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export const EmbeddingsBlock: BlockConfig<EmbeddingsResponse> = {
157157
description: 'Generate embeddings',
158158
authMode: AuthMode.ApiKey,
159159
longDescription:
160-
'Turn text into embedding vectors for semantic search, clustering, and similarity. Supports OpenAI, OpenRouter, Google Gemini, Cohere, and Mistral embedding models, plus any model on a self-hosted Ollama.',
160+
'Turn text into embedding vectors for semantic search, clustering, and similarity. Supports OpenAI, OpenRouter, Google Gemini, Cohere, and Mistral embedding models, plus embedding models on a self-hosted Ollama.',
161161
category: 'tools',
162162
integrationType: IntegrationType.AI,
163163
docsLink: 'https://docs.sim.ai/integrations/embeddings',

apps/sim/lib/embeddings/catalog.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ export const KB_EMBEDDING_STORAGE_DIMENSIONS = [3072, 1536, 1024, 768, 384] as c
3030

3131
export type KbEmbeddingDimensions = (typeof KB_EMBEDDING_STORAGE_DIMENSIONS)[number]
3232

33+
/**
34+
* Widest width a knowledge base can be created at. Anything sized for "the
35+
* largest response a base could produce" has to use this rather than the
36+
* default, because the per-request item ceiling falls as the width grows.
37+
*/
38+
export const MAX_KB_EMBEDDING_DIMENSIONS: KbEmbeddingDimensions = KB_EMBEDDING_STORAGE_DIMENSIONS[0]
39+
3340
/**
3441
* Width a knowledge base is created at when the deployment names no other one.
3542
* Matches the `embedding.embedding` column every base predating multi-width
@@ -286,7 +293,12 @@ export function getEmbeddingModelInfo(model: string): EmbeddingModelInfo {
286293

287294
export function findEmbeddingModelInfo(model: string): EmbeddingModelInfo | undefined {
288295
if (isOllamaEmbeddingModel(model)) return buildOllamaEmbeddingModelInfo(model)
289-
return EMBEDDING_MODELS[model]
296+
/**
297+
* Own-property lookup, not indexing: the record's prototype is
298+
* `Object.prototype`, so `EMBEDDING_MODELS['toString']` would otherwise hand
299+
* back an inherited function that every downstream field read then crashes on.
300+
*/
301+
return Object.hasOwn(EMBEDDING_MODELS, model) ? EMBEDDING_MODELS[model] : undefined
290302
}
291303

292304
export function getModelsForProvider(provider: KeyedEmbeddingProvider): string[] {

apps/sim/lib/embeddings/client.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -972,13 +972,17 @@ export async function embedKnowledgeForDeployment(
972972
const capabilityValues = {
973973
...env,
974974
/**
975-
* The capability gates its providers on the family `KB_EMBEDDING_MODEL`
976-
* names, but what matters here is the model this call actually embeds with:
977-
* a knowledge base keeps the model it was created with, so one created
978-
* before the deployment default changed must still resolve its own family's
979-
* transports. Substituting it evaluates the chain for the model at hand.
975+
* The capability gates its providers on the model and width
976+
* `KB_EMBEDDING_MODEL` and `EMBEDDING_OUTPUT_DIMS` name, but what matters
977+
* here is the target this call actually embeds with: a knowledge base keeps
978+
* the model and width it was created with, so one created before the
979+
* deployment default changed must still resolve its own family's transports.
980+
* Both are substituted — the model alone would leave the deployment's width
981+
* being validated against this base's family, which rejects the chain
982+
* outright for a base whose family accepts a width the deployment's does not.
980983
*/
981984
KB_EMBEDDING_MODEL: model,
985+
EMBEDDING_OUTPUT_DIMS: String(dimensions),
982986
...(workspaceKey ? { OPENAI_API_KEY: workspaceKey.apiKey } : {}),
983987
}
984988

apps/sim/lib/embeddings/knowledge-embedding-family.test.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getEmbeddingModelInfo,
99
getKbEligibleModels,
1010
} from '@/lib/embeddings/catalog'
11+
import { isKbEmbeddingModel } from '@/lib/knowledge/embedding-models'
1112

1213
/**
1314
* `knowledgeEmbeddingFamily` decides which credential the setup CLI and the
@@ -28,16 +29,36 @@ describe('knowledgeEmbeddingFamily', () => {
2829
})
2930

3031
it('classifies any model on the deployment’s own Ollama by its routing prefix', () => {
31-
for (const model of ['ollama/nomic-embed-text', 'ollama/mxbai-embed-large:335m', 'OLLAMA/x']) {
32+
for (const model of ['ollama/nomic-embed-text', 'ollama/mxbai-embed-large:335m']) {
3233
expect(knowledgeEmbeddingFamily({ KB_EMBEDDING_MODEL: model }), model).toBe('ollama')
3334
}
3435
})
3536

36-
it('falls back to the family of the model an unset variable defaults to', () => {
37+
/**
38+
* The classifier decides which credential the CLI reports as serving knowledge
39+
* embeddings; the runtime decides which one actually gets used. An id the
40+
* runtime rejects falls back to the default model, so the classifier has to
41+
* call it that family too — otherwise a deployment holding only the credential
42+
* it names passes its status check and fails every embedding call.
43+
*/
44+
it('agrees with the runtime on ids the runtime does not accept', () => {
3745
const defaultFamily = getEmbeddingModelInfo(DEFAULT_EMBEDDING_MODEL).provider
46+
const rejected = [
47+
'',
48+
' ',
49+
'not-a-model',
50+
'gemini-embedding-999',
51+
'Gemini-Embedding-001',
52+
'gemini',
53+
'OLLAMA/nomic-embed-text',
54+
'ollama/',
55+
'toString',
56+
'constructor',
57+
]
58+
for (const model of rejected) {
59+
expect(isKbEmbeddingModel(model), `${model} must not be a KB model`).toBe(false)
60+
expect(knowledgeEmbeddingFamily({ KB_EMBEDDING_MODEL: model }), model).toBe(defaultFamily)
61+
}
3862
expect(knowledgeEmbeddingFamily({})).toBe(defaultFamily)
39-
expect(knowledgeEmbeddingFamily({ KB_EMBEDDING_MODEL: ' ' })).toBe(defaultFamily)
40-
/** An unrecognised id falls back to the default model, which is OpenAI's. */
41-
expect(knowledgeEmbeddingFamily({ KB_EMBEDDING_MODEL: 'not-a-model' })).toBe(defaultFamily)
4263
})
4364
})

apps/sim/lib/embeddings/ollama-model-catalog.server.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ export class OllamaEmbeddingModelNotFoundError extends Error {
3636
}
3737
}
3838

39+
/** The configured server could not be reached at all — an outage, not a bad model id. */
40+
export class OllamaUnreachableError extends Error {
41+
constructor(cause: string) {
42+
super(`The configured Ollama server could not be reached: ${cause}`)
43+
this.name = 'OllamaUnreachableError'
44+
}
45+
}
46+
3947
export class OllamaEmbeddingWidthUnknownError extends Error {
4048
constructor(model: string) {
4149
super(
@@ -88,12 +96,23 @@ function readEmbeddingLength(modelInfo: Record<string, unknown> | undefined): nu
8896
export async function fetchOllamaEmbeddingModelCatalog(
8997
signal?: AbortSignal
9098
): Promise<OllamaEmbeddingModel[]> {
99+
return (await loadOllamaEmbeddingModelCatalog(signal)).models
100+
}
101+
102+
/**
103+
* The catalog plus why it is empty, so a caller resolving one specific model can
104+
* tell "not installed" apart from "no server answered". The selector only needs
105+
* the list; the tool needs the distinction to pick a status code.
106+
*/
107+
async function loadOllamaEmbeddingModelCatalog(
108+
signal?: AbortSignal
109+
): Promise<{ models: OllamaEmbeddingModel[]; unreachable?: string }> {
91110
/**
92111
* Hosted Sim runs no Ollama, and the loopback default cannot answer there, so
93112
* an unconfigured hosted deployment is not dialled at all. An explicit
94113
* `OLLAMA_URL` states an intent to reach a real server and is still honoured.
95114
*/
96-
if (isHosted && !isOllamaUrlConfigured()) return []
115+
if (isHosted && !isOllamaUrlConfigured()) return { models: [] }
97116

98117
let names: string[]
99118
try {
@@ -103,10 +122,9 @@ export async function fetchOllamaEmbeddingModelCatalog(
103122
names = tags.models.map((model) => model.name)
104123
} catch (error) {
105124
signal?.throwIfAborted()
106-
logger.info('Ollama is not reachable; offering no embedding models', {
107-
error: getErrorMessage(error, 'Unknown error'),
108-
})
109-
return []
125+
const cause = getErrorMessage(error, 'Unknown error')
126+
logger.info('Ollama is not reachable; offering no embedding models', { error: cause })
127+
return { models: [], unreachable: cause }
110128
}
111129

112130
const resolved = await mapWithConcurrency(names, OLLAMA_SHOW_CONCURRENCY, async (name) => {
@@ -132,7 +150,7 @@ export async function fetchOllamaEmbeddingModelCatalog(
132150
}
133151
})
134152

135-
return resolved.filter((model): model is OllamaEmbeddingModel => model !== null)
153+
return { models: resolved.filter((model): model is OllamaEmbeddingModel => model !== null) }
136154
}
137155

138156
/**
@@ -149,9 +167,11 @@ export async function getOllamaEmbeddingModelMetadata(
149167
signal?: AbortSignal
150168
): Promise<Required<OllamaEmbeddingModel>> {
151169
const name = isOllamaEmbeddingModel(model) ? ollamaEmbeddingModelName(model) : model
152-
const catalog = await fetchOllamaEmbeddingModelCatalog(signal)
170+
const { models, unreachable } = await loadOllamaEmbeddingModelCatalog(signal)
171+
/** An empty catalog because nothing answered is an outage, not a missing model. */
172+
if (unreachable !== undefined) throw new OllamaUnreachableError(unreachable)
153173
/** Ollama resolves a bare name to its `:latest` tag, so both spellings match. */
154-
const metadata = catalog.find(
174+
const metadata = models.find(
155175
(candidate) => candidate.id === name || candidate.id === `${name}:latest`
156176
)
157177
if (!metadata) throw new OllamaEmbeddingModelNotFoundError(name)

apps/sim/lib/internal/embeddings/operations.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ export async function executeEmbedding(
9696
.dimensions
9797
} catch (error) {
9898
context.signal?.throwIfAborted()
99+
/**
100+
* A model the caller can fix (not installed, or one whose width Ollama
101+
* will not report) is a 400; anything else — an unreachable server above
102+
* all — is an upstream failure and must not read as a bad request.
103+
*/
99104
const userError =
100105
error instanceof OllamaEmbeddingModelNotFoundError ||
101106
error instanceof OllamaEmbeddingWidthUnknownError

0 commit comments

Comments
 (0)