Skip to content

feat(embeddings): multi-provider Embeddings block on a shared core - #6317

Merged
waleedlatif1 merged 29 commits into
stagingfrom
feat/embeddings-multi-provider
Aug 6, 2026
Merged

feat(embeddings): multi-provider Embeddings block on a shared core#6317
waleedlatif1 merged 29 commits into
stagingfrom
feat/embeddings-multi-provider

Conversation

@mzxchandra

Copy link
Copy Markdown
Contributor

What

Adds a multi-provider Embeddings block (OpenAI, Gemini, Cohere, Mistral) and extracts the embedding engine into a shared core that both the block and knowledge-base indexing consume.

The knowledge-base path already had a real multi-provider engine — BYOK→env→rotating-key resolution, token-aware batching, retry, L2 normalization. The block had a bare fetch with none of it. Nothing bridged the two, so the block couldn't reach Gemini and the KB engine couldn't be reached from a workflow. This extracts the shared core first, then builds breadth on top, rather than adding a third parallel implementation.

Shape

  • lib/embeddings/ — catalog (single source of truth for 7 models), client, key resolution, batching, L2 normalization, and adapters for OpenAI, Azure OpenAI, Gemini, Cohere, Mistral
  • lib/knowledge/embeddings.ts — now a thin wrapper; exported signatures unchanged, and the 1536-dimension pgvector invariant does not move
  • tools/embeddings/ — one tool per provider from a shared factory, behind a single /api/tools/embeddings route and contract
  • New embeddings block type. The openai block is left functionally untouched and only leaves the discovery surfaces via hideFromToolbar + sunset.replacedBy, so placed instances keep working with no migration. openai_embeddings is now an alias of embeddings_openai, so legacy instances pick up batching, retry, and metering with no visible change.

Verification

Live provider matrix, 16/16 against real APIs across all four providers. Each case asserts vector count, input ordering (two identical inputs must return cosine ≈ 1.0 while an unrelated one stays far below), emitted and reported dimensionality, L2 norm, and non-zero token usage. Gemini's Matryoshka reduction is normalized at both 1536 and 768.

Three bugs were found by that matrix and by manual testing, each fixed with a regression test:

  • Every unreduced request to text-embedding-ada-002 and mistral-embed failed with a provider 400. resolveDimensions returns the native size when no reduction is requested, and that concrete value reached the adapter, so the field was always sent. Models supporting Matryoshka accept their own native size, which hid it for 4 of 6 models; the two with no supportedDimensions reject the parameter outright.
  • A stale dimensions or taskType survived a model switch. The per-model dropdowns share one subblock id and nothing clears a stored value when dependsOn fields change, so a choice made for one model was forwarded for another.
  • An unsupported dimensions returned 502 instead of 400 — a client input error reported as an upstream failure.

Also: 3657 tests passing, typecheck clean across 23 tasks, check:api-validation passing.

Reviewer attention

Model-input provenance (#6247). That commit added secret projection to the two files this branch rewrote. A textual merge would have compiled, passed CI, and silently dropped the control — the projection entry gate is fail-open (if (!registry || modelInput?.mode !== 'project') return params). Carried through as:

  • the tool factory declares request.modelInput, covering all four provider tools and the legacy alias
  • embed() takes a projectInputs projector that the KB wrapper supplies, preserving projectKnowledgeModelInputs and its use of projected values for token estimation

projectInputs is required and explicitly nullable rather than optional, so a new caller can't omit it silently — that's the same fail-open shape as the bug above. The route passes null because prepareToolRequest already projected at the HTTP hop. Projection runs once outside the retry loop. This makes EmbedOptions a source-breaking change for any future embed() caller, deliberately.

@vikhyathvikku — the projector threading is a design call inside your feature's territory, made by someone shipping an unrelated block. Worth your eyes.

Not in this PR

A platform fix for copilot edit-workflow validation, which resolves same-id conditional subblock variants instead of silently using the last-declared one. The embeddings block surfaced it, but it affects ~20 blocks (video_generator.duration has 12 variants) and it narrows what programmatic edits accept, so it ships separately. Until then, a programmatic edit to an embeddings block validates model/dimensions against the last-declared provider variant. The block is unaffected in the editor and at runtime.

Testing done

KB index + search regression confirmed. Still worth a look post-merge: discovery surfaces (legacy block absent from toolbar/search/mentions, new one present) and run-log cost attribution on a hosted-key vs BYOK run.

The Embeddings block was OpenAI-only with a bare fetch: no batching, no
retry, no metering, and no hosted-key support. Meanwhile the knowledge-base
indexing path already had a real multi-provider engine. Nothing bridged the
two, so the block could not reach Gemini and the KB engine could not be
reached from a workflow.

Extract the shared core into lib/embeddings/ first, then build breadth on
top of it, so both the KB path and the block resolve models and providers
from one catalog and one set of adapters instead of a third parallel
implementation.

- lib/embeddings/: catalog, client, key resolution, batching, L2
  normalization, and adapters for OpenAI, Azure OpenAI, Gemini, Cohere,
  and Mistral
- lib/knowledge/embeddings.ts becomes a thin KB wrapper with its exported
  signatures unchanged; the 1536-dimension vector invariant does not move
- one tool per provider from a shared factory, behind a single
  /api/tools/embeddings route and contract
- new `embeddings` block type; the `openai` block is left functionally
  untouched and only leaves the discovery surfaces via hideFromToolbar
  plus sunset.replacedBy, so placed instances keep working unmigrated
- openai_embeddings is now an alias of embeddings_openai, so legacy
  instances pick up batching, retry, and metering with no visible change
The route validated the model and the provider match up front but left
`dimensions` to be checked inside embed(), where resolveDimensions throws
and the generic catch maps it to 502. A typo in the block's dimension
field, or a reference expression resolving to an out-of-range value, was
reported as an upstream gateway failure rather than bad input.

Resolve dimensions in the route alongside the other boundary checks and
return 400. The throw stays the single source of the message, so the two
call sites cannot drift.

Adds route tests covering auth, the response shape, each boundary
rejection, input normalization, and the 502 path for genuine provider
failures.
resolveDimensions() returns the model's native size when no reduction is
requested, and that resolved value was handed straight to the adapter. The
adapters guard on `dimensions !== undefined`, so the field was always
populated and always sent.

Models that support Matryoshka reduction accept their own native size, so
this was invisible for text-embedding-3-*, gemini-embedding-001,
embed-v4.0, and codestral-embed. Models that do not support the parameter
at all reject it outright: every unreduced request to text-embedding-ada-002
and mistral-embed failed with a 400, which is both of the models whose
catalog entry has no supportedDimensions.

Track the caller's explicit reduction separately from the resolved
dimensionality. The resolved value still drives reporting and billing; only
the requested one reaches the wire.

Found by driving the live provider matrix against all four providers.
Every test dynamically imported the module under test, so the first one to
run paid the whole cold-load cost inside its own 10s timeout and failed
intermittently under load.

The dynamic imports were working around a hoisting problem: mockMapTags is
a top-level const read by a vi.mock factory, and vi.mock is hoisted above
it, so a static import of the module under test crashes with a
use-before-initialization error. Declaring the mock through vi.hoisted()
removes that constraint, which is the pattern the testing guidelines
already call for.

One static import replaces 42 dynamic ones. The file drops from ~15s to
~2s and passed 5 consecutive runs.
The per-model Dimensions and Task Type dropdowns each share one subblock
id, and nothing clears a stored subblock value when its dependsOn fields
change — dependsOn only feeds rendering. A choice made for one model
therefore outlives a switch to another.

Picking 3072 on text-embedding-3-large and switching to -3-small left 3072
stored while the dropdown offered at most 1536, and the block forwarded it.
Same for a task type: 'similarity' chosen on Gemini survived a switch to
Cohere, which has no equivalent input type.

The guards only checked that the model declared the capability at all, not
that the value was one it lists. Check membership so a stale value falls
back to the model's native size, or is omitted, instead of being sent and
rejected. The user cannot have deliberately chosen an option the dropdown
stopped presenting.
Replaces the scatter-plot-on-axes placeholder with a centre node, four
neighbours, and the rays between them — a point and its nearest neighbours
in embedding space, which is what the block actually produces. The axes
mark read as a generic chart and said nothing specific to embeddings.

Nodes are filled so they hold their shape at small sizes. The rays carry
less weight than the nodes to keep the hierarchy, but at 1.6/0.9 rather
than the 1.4/0.75 they were drawn at, so they do not thin out to loose
dots in the 14px block-search row.

Kept byte-identical between the app and docs icon sets.
openai_embeddings became an alias of embeddings_openai, so the legacy
block's runtime payload gained `provider` and `dimensions`. Its declared
outputs still listed only embeddings/model/usage, so the tag picker never
offered two fields every run demonstrably returns, and downstream blocks
could not reference them.

Declaring them is additive and does not touch execution. Asserts the
legacy block's output keys match the replacement's, since both run the
same tool and neither should expose fields the other lacks.
A block may declare one field id several times, each variant conditioned
on another field — the embeddings block declares model, dimensions, and
taskType once per provider, and the image and video generators do the
same. Validation keyed a map by id alone, so whichever variant was
declared last silently became the validator for every write to that
field.

Programmatic edits to an embeddings block were therefore checked against
Mistral's option lists whatever the saved provider: `text-embedding-3-small`
was rejected as not one of mistral-embed/codestral-embed, and dimensions
valid only elsewhere (3072, 768) could not be set at all. Values that
happened to overlap the last variant passed, so automation saw partial
success rather than a clean failure.

Keep every candidate per id and pick the one whose condition holds,
evaluating against the mutation's inputs merged over the block's saved
values so a partial write still resolves. When no condition matches, fall
back to the union of all variants' options rather than guessing.

Conditions still never gate whether a field may be written — that was a
deliberate choice and a hidden field stays writable. They only select
which definition describes the field, and an unresolved condition widens
the accepted set instead of narrowing it.
…h-all

An unconditioned same-id variant matches every set of values, so it would
shadow a genuinely selected variant purely by being declared first. Prefer
a variant that actually asserted something about the current values.

No block in the registry currently declares a catch-all ahead of a
conditioned variant on a field where it would change validation, so this
is a guard against the pattern rather than a fix for a live case.
Carries staging's model-input provenance (#6247) through the embeddings
refactor. That commit added secret projection to the two files this branch
rewrote, so a textual "keep ours" would have compiled, passed CI, and
silently dropped the control on both paths — the projection entry gate is
fail-open (`if (!registry || modelInput?.mode !== 'project') return params`).

Block/tool path: the factory now declares `request.modelInput`, so all four
provider tools and the legacy `openai_embeddings` alias project `input`
before the request is built — matching what staging declared on the tool
this branch replaced.

Knowledge-base path: `embed()` takes a `projectInputs` projector that the
KB wrapper supplies, preserving staging's `projectKnowledgeModelInputs`
call and its use of projected values for token estimation. The projector is
required and explicitly nullable rather than optional, so a new caller
cannot omit it silently; the tool route passes null because
`prepareToolRequest` already projected at the HTTP hop.

Projection runs once outside the retry loop so a retry cannot re-project
already-projected content.
Two changes made while building the Embeddings block are not part of it and
ship separately, so their files are restored to staging here:

- copilot edit-workflow validation resolving same-id conditional subblock
  variants. The embeddings block surfaced it, but it is a platform fix
  affecting ~20 blocks that declare a field id more than once, and it
  narrows what programmatic edits accept — that deserves its own review.
- the sync-engine test de-flake, which is unrelated test hygiene.

Both are preserved in full on feat/embeddings-full-snapshot.

Note this restores the reported bug where a programmatic edit to an
embeddings block validates model/dimensions against the last-declared
provider variant. The block is unaffected in the editor and at runtime.
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 6, 2026 10:14pm

Request Review

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches shared embedding execution for KB indexing and new external API routes with BYOK/hosted keys and billing hooks; legacy paths are preserved but the refactor surface is large.

Overview
Introduces a multi-provider Embeddings workflow block (OpenAI, Gemini, Cohere, Mistral) and centralizes embedding logic in lib/embeddings/ so the block and knowledge-base indexing share one catalog, client, adapters, and key resolution.

The new POST /api/tools/embeddings route and per-provider tools (from a shared factory) back the block. lib/knowledge/embeddings.ts is reduced to a thin wrapper around embed(), still pinning KB vectors at 1536 dimensions.

The legacy openai embeddings block stays executable but is hidden from discovery (hideFromToolbar, sunset → embeddings); openai_embeddings aliases embeddings_openai so existing workflows gain batching, retry, and metering. Legacy block outputs now declare provider and dimensions.

Docs add integrations/embeddings, icons/mappings/registry updates, pricing for new models, and several integration docs drop empty pricing/rateLimit param rows. Vector-store block templates reference embeddings instead of openai where appropriate.

Reviewed by Cursor Bugbot for commit ae002f1. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR introduces a shared multi-provider embedding core used by both workflow tools and knowledge-base indexing, while preserving the fixed-width knowledge-base vector contract.

  • Adds OpenAI, Azure OpenAI, Gemini, Cohere, and Mistral adapters with shared key resolution, batching, retry, normalization, and dimensionality handling.
  • Adds the multi-provider Embeddings block, provider tools, API contract and route, registry metadata, documentation, and compatibility handling for legacy OpenAI embedding workflows.
  • Retains model-input projection before token measurement and uses catalog-specific input ceilings without the previously introduced safety-margin truncation.

Confidence Score: 5/5

The PR appears safe to merge because the previously reported batching, projection-order, reduced-ceiling, and comment-convention concerns are fixed or invalid at the current head.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/embeddings/client.ts Implements the shared embedding lifecycle, including projection-before-batching, model-specific input ceilings, bounded concurrency, retries, and result aggregation; the previously reported batching and projection defects are addressed.
apps/sim/lib/embeddings/catalog.ts Centralizes provider ownership, model dimensions, task capabilities, token ceilings, defaults, pricing identifiers, and knowledge-base eligibility.
apps/sim/lib/knowledge/embeddings.ts Replaces duplicated provider logic with a thin shared-core wrapper while retaining model validation, projection, billing metadata, and the fixed knowledge-base vector width.
apps/sim/app/api/tools/embeddings/route.ts Adds authenticated request validation, normalized-input bounds, model/provider checks, dimensionality validation, and stable response metering.
apps/sim/blocks/blocks/embeddings.ts Adds the multi-provider block configuration with catalog-derived models and capability-aware parameter forwarding.
apps/sim/tools/embeddings/factory.ts Defines the shared provider-tool configuration and model-input projection contract used by the workflow execution path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Workflow[Embeddings workflow block] --> Tool[Provider-specific embedding tool]
  Tool --> Route[Embeddings API route]
  Knowledge[Knowledge indexing and search] --> Wrapper[Knowledge embedding wrapper]
  Route --> Core[Shared embedding core]
  Wrapper --> Core
  Core --> Catalog[Model catalog and limits]
  Core --> Keys[BYOK / environment / rotating-key resolution]
  Core --> Batch[Projection, truncation, batching, retry]
  Core --> Adapter{Provider adapter}
  Adapter --> OpenAI[OpenAI / Azure OpenAI]
  Adapter --> Gemini[Gemini]
  Adapter --> Cohere[Cohere]
  Adapter --> Mistral[Mistral]
  Wrapper --> VectorStore[(1536-dimension KB vectors)]
Loading

Reviews (9): Last reviewed commit: "fix(docs): generate tool inputs for fact..." | Re-trigger Greptile

Comment thread apps/sim/lib/embeddings/client.ts Outdated
Comment thread apps/sim/app/api/tools/embeddings/route.ts
Comment thread apps/sim/app/api/tools/embeddings/route.ts
Comment thread apps/sim/app/api/tools/embeddings/route.ts
…t path

Review round 1.

Batching used one 8,000-token constant for every model, inherited from the
knowledge-base engine this branch extracted. `batchByTokenLimit` truncates
any single text above the limit it is given, so that constant both sent
oversized input to models with a lower ceiling and silently dropped content
models with a higher one accept:

- Gemini declares 2,048, so a 3,000-token text passed through whole and the
  provider rejected it, surfacing as a 502. This also affected knowledge-base
  indexing on staging, which uses the same constant.
- Cohere declares 128,000, so anything past 8,000 was truncated for no reason.

Batch against the selected model's own `maxInputTokens` instead. Using the
per-input ceiling as the per-batch budget also keeps every individual text
within it.

The contract bounds the array arm of `input`, but a JSON-encoded array
arrives as a plain string and `normalizeInput` only expands it after
validation — so neither the 1,000-input cap nor the non-empty checks applied
to the reference-expression path the route was written to accept. `"[]"`
also reported success with no vectors. Re-check the normalized list so the
bounds hold for both shapes.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

CI's tool-metadata:check gate failed: registering embeddings_openai,
embeddings_gemini, embeddings_cohere, and embeddings_mistral left the
generated tool-ids/metadata/outputs artifacts stale.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread apps/sim/lib/embeddings/client.ts Outdated
Comment thread apps/docs/components/ui/icon-mapping.ts
… docs icon

Review round 2.

Projection ran inside callEmbeddingAPI, after batchByTokenLimit had already
measured and truncated the original text. The projector rewrites resolved
secrets to placeholders, which changes length, so batching sized against a
string that was never sent: a lengthening projection then pushed input past
the model's ceiling and the provider rejected it, and a shortening one
discarded document content that would have fit.

Project once up front, then batch the projected text, so truncation measures
what actually goes to the provider. This also keeps projection to exactly one
call per embed(), so no retry can re-project.

Separately, marking the legacy openai block hideFromToolbar dropped it from
the generated docs icon map, which only retains hidden blocks when they are
versioned. integrations/openai.mdx is deliberately kept — docsLink is baked
into every placed instance — so BlockInfoCard lost its icon and fell back to
a text tile. A sunset block keeps its docs page for the same reason a hidden
versioned block does, so the generator now treats it the same way.

The sim-side integrations map still omits it, which is intended: that feeds
the discovery page a sunset block should not appear on, and placed blocks
render from the registry's own icon reference.
…eign

Review round 4.

Batching measures with tiktoken, which only has encodings for OpenAI models —
every other id falls back to cl100k_base. Gemini's 2048, Cohere's 128k, and
Mistral's 8192 were therefore enforced in OpenAI token units, so an input near
one of those ceilings could still be rejected upstream or trimmed more than
needed.

A true fix needs per-provider tokenizers, which the repo does not have:
estimateTokenCount is a chars-per-token heuristic, and truncation needs a real
encode/decode pair to slice on a token boundary. So the ceiling is discounted
for foreign tokenizers rather than trusted exactly.

The discount is one-sided on purpose. Overshooting means the provider rejects
the whole request; undershooting only trims a text that was already at the
limit, so the margin errs toward the second.

resolveBatchTokenCeiling is a pure function tested directly, rather than
inferred from truncation behavior, so the guarantee holds per model as the
catalog grows.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread apps/sim/lib/embeddings/catalog.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 4da0625. Configure here.

Review round 5. Reverts the safety margin from round 4.

The two review findings were in direct tension: round 4 flagged that a
foreign model's ceiling is measured in tiktoken units, and the margin added
to absorb that error reintroduced the round 3 harm — valid content truncated
below the provider's declared limit.

The margin was the wrong trade. It swapped a loud failure for a silent one:
an undercount surfaces as a provider rejection the caller can see and act on,
while shortening an embedding's input produces a degraded vector that is
indistinguishable from a good one at every layer above it. Silent quality
loss in a retrieval index is the worse outcome, and it is also the harder one
to ever notice.

So the declared ceiling is applied exactly, and truncation is no longer
silent: an input above the limit now logs a warning naming the model, the
limit, and whether the count was approximate. hasApproximateTokenCount
records which models are counted with a foreign tokenizer without being used
to shrink anything.

The tokenizer imprecision itself remains, and cannot be fixed without
per-provider BPE the repo does not have — estimateTokenCount is a
chars-per-token heuristic, and truncation needs a real encode/decode pair to
slice on a token boundary.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e742ac9. Configure here.

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@waleedlatif1

Audit follow-ups on the multi-provider embeddings work:

- Enforce OpenAI's documented 2048-entry `input` array cap in the OpenAI and
  Azure adapters. Nothing bounded item count on the OpenAI path — batching
  bounds tokens per request, so a batch of many short inputs could exceed it.
- Make the provider item cap single-source. It was declared both on the catalog
  entry and on the adapter, read through a `??`; the adapter is the wire-protocol
  owner, so the catalog copy is gone.
- Have the knowledge-base view call `getKbEligibleModels()` instead of
  re-deriving the same `kbEligible` filter inline.
- Remove dead surface: the unused `EMBEDDING_TASK_TYPES` constant,
  `EmbeddingToolDefinition`, `HOSTED_KEY_PROVIDERS`, and the five request-body
  fields (`workspaceId`, `workflowId`, `executionId`, `userId`,
  `useHostedCostTracking`) the route never reads.
- Trim `@/lib/embeddings` to what callers outside the module use.
- Drop the route's manual request-id plumbing; `withRouteHandler` supplies it.
- Fix two comments that had drifted onto the wrong declaration.
…n ceiling

Second validation pass against provider documentation.

- Cohere: normalize locally when `output_dimension` reduces below native.
  Cohere documents the parameter as Matryoshka truncation but never states that
  it renormalizes, and an unnormalized vector silently skews cosine similarity.
  `l2Normalize` is idempotent, so this is a no-op if Cohere already returns unit
  vectors and a correctness fix if it does not. Covered by a test that fails
  without it.
- OpenAI: raise the per-input ceiling from 8191 to the 8192 the API reference
  documents, so a maximal input is no longer truncated by one token.
- Share the OpenAI response type with the Azure adapter instead of declaring an
  identical copy, mirroring how the mail providers share `_nodemailer`.
- Rewrite the Gemini item-cap comment to say the 100-item limit is observed
  rather than documented, which is what Google's reference actually supports.

Docs: add a manual intro to the Embeddings page covering providers, models,
inputs, outputs, and comparability rules. The generated Input tables are empty
because `createEmbeddingTool` builds params programmatically and the docs
generator only reads literals, so the manual section carries that reference.
…provider gaps

Four gaps found in the validation pass.

Gemini token counts were estimated, not measured. `BatchEmbedContentsResponse`
carries `usageMetadata.promptTokenCount`; without reading it the client fell back
to tiktoken, which has no Gemini encoding and silently used `cl100k_base` — the
wrong tokenizer on a count knowledge-base runs bill against.

`maxInputTokens` was doing two jobs: the per-input ceiling that decides
truncation, and the per-request budget that decides how many inputs share a
batch. These are different provider limits, and conflating them meant Cohere
packed batches against its 128k per-document ceiling while OpenAI's documented
300,000-token request cap went unenforced. They are now separate fields.

Truncation moves out of `batchByTokenLimit` and into `embed`, so it happens once,
against the per-input ceiling, and always logs. The request budget is floored at
that ceiling — a budget below it would truncate inputs the provider accepts.
Batch sizes are unchanged everywhere except Gemini, which rises from 2048 to the
8192 the other providers already used.

codestral-embed now offers its documented 3072 maximum. Its API default is 1536,
so the offered sizes straddle the default; the catalog invariant relaxes from
"native size first" to "native size present", which is what the block relies on.

The Mistral API-key field no longer differs from the other three. Sim stocks
`MISTRAL_API_KEY` — `mistral_parse` already hides its key field on hosted — so
one field with `hideWhenHosted` replaces the conditional pair.

Docs: correct the API-key row, which described the old Mistral-only behavior.
…ed helpers

Findings from a four-angle quality review.

Reuse: `splitByItemLimit` and `processWithConcurrency` were reimplementations of
`chunkArray` (`@sim/utils`) and `mapWithConcurrency`
(`@/lib/core/utils/concurrency`), so `lib/embeddings/batching.ts` is gone. That
helper's doc forbade a throwing mapper; embedding legitimately wants a failed
batch to fail the call, since a partial vector set is not a usable result, so the
contract is reworded to cover both intents rather than forked.

The block no longer hand-copies the catalog. Its model, task-type, and dimension
dropdowns are derived from `EMBEDDING_MODELS`, which deletes roughly 150 lines of
literals that had to be kept in step by a drift test. The comment claiming this
was impossible was wrong: `generate-docs.ts` only reads `subBlocks` looking for
an `id: 'operation'` entry, which this block does not have. Verified by
regenerating — `embeddings.mdx` and `integrations.json` come out byte-identical.

Single-sourced two maps that were stated twice: BYOK provider ids (which encode
the non-obvious gemini -> google mapping) and the per-provider default model.
The route previously took its default from `getModelsForProvider(provider)[0]`,
which silently depended on catalog key order.

Azure's `endpoint` and `apiVersion` are required on their own context type
instead of optional on the shared one, so the adapter can no longer be built
without them and emit an `undefined/...` URL.

Also: contract enums now `satisfies` the catalog unions so they cannot drift,
the barrel exports only what callers outside the module use, the redundant
`requestedDimensions` field is a parameter, the bare `getEmbeddingModelInfo()`
call is a named `assertKbEmbeddingModel`, and the route checks payload size
before scanning entries rather than copying the body first.
A comment pass over the feature found four that no longer matched what they sat
on, all introduced by earlier rounds of this work.

The contract's `satisfies` note promised that adding a catalog provider could
not leave the wire enum stale. It cannot deliver that: `satisfies` proves every
listed member is valid, not that the list is exhaustive, so an addition stays
silently absent. Reworded to say what it does and does not catch.

The client cited Gemini as a provider that omits usage, which the Gemini adapter
now contradicts — it reads `usageMetadata.promptTokenCount`. Every adapter
defines `parseTokens`, so the fallback is about a response lacking a usage block,
not about a particular provider.

`l2Normalize` documented only Gemini, though Cohere now calls it for a different
and stronger reason, and "normalizes in place" read as mutation when the function
returns a copy.

The route's new size-guard comment claimed it avoids copying the payload; nothing
there copies. The real reason is that summing lengths gates before the per-entry
character scan.

Also: split the derived-sub-block TSDoc so both constants carry hover text, gave
the payload cap its own doc, dropped one comment that restated a signature, and
tightened two long blocks without losing a fact.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit dd636b8. Configure here.

The four embeddings tools rendered header-only Input tables. `extractToolInfo`
finds a tool's `params` by regex over the tool's own file, and these files hold
nothing but a `createEmbeddingTool({...})` call — the params live in the
factory's module. There was already a fallback for a same-file `...spread` base,
so this adds the cross-module equivalent: follow the factory's import and read
`params` from there.

Two things surfaced once the tables populated.

`hosting` was not in the set of keys that terminate the `params` capture, so the
non-greedy match ran past it to `request:` and swallowed the whole hosting block.
Every tool with a `hosting:` section between `params:` and `request:` was
publishing `pricing` and `rateLimit` as if they were user-facing inputs — this
drops those rows from eight unrelated integration pages as well.

The shared apiKey description was a template literal, which the regex emitted
verbatim as `${name} API key`. It is now a static string, matching how every
other tool in the repo declares one.

Docs: the Embeddings page keeps a prose intro in its MANUAL-CONTENT block like
other integrations, with the hand-written input/output tables removed now that
the generated ones are correct. The sunset `openai` page loses its
`encodingFormat` row — page generation skips hidden blocks, so that page is
frozen and would otherwise keep advertising a parameter the aliased tool no
longer accepts.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ae002f1. Configure here.

@waleedlatif1
waleedlatif1 merged commit dc5bab6 into staging Aug 6, 2026
5 checks passed
@waleedlatif1
waleedlatif1 deleted the feat/embeddings-multi-provider branch August 6, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants