Skip to content

embeddings: default check_embedding_ctx_length=False for OpenAI-compa… - #129

Merged
kesmit13 merged 6 commits into
mainfrom
users/kaushik/embed-ctx-length-default
Aug 11, 2026
Merged

embeddings: default check_embedding_ctx_length=False for OpenAI-compa…#129
kesmit13 merged 6 commits into
mainfrom
users/kaushik/embed-ctx-length-default

Conversation

@kkampli-singlestore

@kkampli-singlestore kkampli-singlestore commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Don’t use OpenAI’s tokenizer for Qwen — LangChain’s default (check_embedding_ctx_length=True) turns text into OpenAI token IDs. Qwen can’t read those, so embeddings are garbage. Send raw text instead (False) so the server tokenizes correctly.

Keep OpenAI/Azure on the default — Real OpenAI/Azure models still use True (tiktoken + LangChain’s built-in long-text handling). Only non-OpenAI/Nova models get the raw-text default.

Chunk long Qwen inputs ourselves — With raw text, LangChain won’t split long docs, so the server 400s or truncates. For those models, split into character chunks, embed each, then average into one vector so long text still works.

Test cases:
1.
Before:
label cosine_vs_anchor
anchor 1.0
unrelated2 0.3536
same_intent 0.3461
paraphrase 0.304
unrelated 0.2523

After
image

Chunking Long Text
Screenshot from 2026-07-22 21-56-26


Note

Medium Risk
Changes default embedding behavior for all non-Azure OpenAI-compatible inference paths, which can alter vector quality and similarity for existing Nova/Qwen workloads but fixes incorrect embeddings from wrong tokenization.

Overview
Fixes broken embeddings for OpenAI-compatible non-Azure models (e.g. Qwen on Nova) by stopping LangChain’s tiktoken path: SingleStoreEmbeddingsFactory now defaults check_embedding_ctx_length=False and returns _ChunkedOpenAIEmbeddings so the server receives raw text and tokenizes with the model’s own tokenizer.

Azure-hosted OpenAI models are unchanged — they still use stock OpenAIEmbeddings with check_embedding_ctx_length=True and LangChain’s built-in context handling.

Long inputs on Nova-style models are split into ~24k-character chunks, embedded per chunk, then merged via length-weighted averaging and L2 normalization into one vector per document (sync and async embed_documents). Unit tests cover factory branching, chunk splitting, and reduction behavior.

Reviewed by Cursor Bugbot for commit 7cf344c. Bugbot is set up for automated code reviews on this repo. Configure here.

…tible endpoints

langchain OpenAIEmbeddings defaults to check_embedding_ctx_length=True, which
tokenizes client-side with tiktoken and sends OpenAI token IDs. Non-OpenAI models
behind an OpenAI-compatible endpoint (e.g. Qwen) can't interpret those IDs and
return nonsensical embeddings. Default the flag to False so raw text is sent and
the server tokenizes correctly. No-op for genuine OpenAI models; callers can opt
back in with check_embedding_ctx_length=True. Scoped to the OpenAI path, so the
Bedrock/Amazon branch is unaffected.

Co-authored-by: Cursor <cursoragent@cursor.com>
Only default check_embedding_ctx_length=False for self-hosted / non-OpenAI models
(e.g. Qwen on the 'Nova' platform). Genuine OpenAI/Azure models ('Azure'/'OpenAI')
keep langchain's default (True), preserving correct tiktoken tokenization and
client-side long-input chunking for them.

Co-authored-by: Cursor <cursoragent@cursor.com>

@mgiannakopoulos mgiannakopoulos left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚢 Ship IT!

…models

- Azure (OpenAI) models: keep check_embedding_ctx_length=True so langchain uses
  tiktoken (correct for these models) with the passed model name, incl. its long-
  input chunking.
- Non-OpenAI models (e.g. Qwen on 'Nova'): send raw text (check_embedding_ctx_length
  =False) so the server's own tokenizer is used, and split long inputs into
  character-bounded chunks client-side, embedding each and length-weighted-averaging
  them into one vector per input -- irrespective of the flag -- so long texts never
  hit the server's hard context limit (which otherwise errors or silently truncates).

Output stays 1:1 with input rows; short inputs pass through unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
…False otherwise

Non-OpenAI models (e.g. Qwen on 'Nova') get wrong embeddings from langchain's default
tiktoken tokenization (it sends OpenAI token IDs). Send raw text for them so the server
tokenizes with the model's own tokenizer; keep tiktoken (True) for Azure/OpenAI models,
where it's correct. Long-input chunking deferred.

Co-authored-by: Cursor <cursoragent@cursor.com>
Re-add _ChunkedOpenAIEmbeddings: for non-Azure models (e.g. Qwen on 'Nova'), send raw
text (check_embedding_ctx_length=False) and split long inputs into character-bounded
chunks, embedding each and length-weighted-averaging into one vector per input, so long
texts never hit the server's context limit. Azure keeps tiktoken (True).

Co-authored-by: Cursor <cursoragent@cursor.com>

@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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6def72b. Configure here.

Comment thread singlestoredb/ai/embeddings.py

Copilot AI 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.

Pull request overview

Adjusts the embeddings factory to avoid LangChain/OpenAI tokenization for non-Azure OpenAI-compatible endpoints (e.g., Qwen on Nova), and adds client-side character chunking + weighted reduction so long inputs don’t overflow server context limits.

Changes:

  • Introduces _ChunkedOpenAIEmbeddings to chunk long texts and reduce chunk embeddings back into a single vector per input.
  • Updates SingleStoreEmbeddingsFactory to keep Azure behavior (LangChain tokenization/context handling) while defaulting non-Azure paths to raw-text mode (check_embedding_ctx_length=False) and using _ChunkedOpenAIEmbeddings.
  • Adds unit tests covering factory branching and chunk/reduction behavior (sync + async).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
singlestoredb/ai/embeddings.py Adds a chunking OpenAIEmbeddings subclass and adjusts factory defaults based on hosting platform.
singlestoredb/tests/test_embeddings.py Adds unit tests for the new factory branching and chunked embedding reduction logic.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +82 to +106
sys.modules.pop('singlestoredb.ai.embeddings', None)
sys.modules.pop('_test_embeddings_module', None)

httpx = types.ModuleType('httpx')
httpx.Client = MockClient
httpx.Timeout = MockTimeout
sys.modules['httpx'] = httpx

langchain_openai = types.ModuleType('langchain_openai')
langchain_openai.OpenAIEmbeddings = MockOpenAIEmbeddings
sys.modules['langchain_openai'] = langchain_openai

langchain_aws = types.ModuleType('langchain_aws')
langchain_aws.BedrockEmbeddings = MockBedrockEmbeddings
sys.modules['langchain_aws'] = langchain_aws

botocore = types.ModuleType('botocore')
botocore.UNSIGNED = 'unsigned'
sys.modules['botocore'] = botocore

botocore_config = types.ModuleType('botocore.config')
botocore_config.Config = MockConfig
sys.modules['botocore.config'] = botocore_config

sys.modules['boto3'] = MockBoto3('boto3')
Comment on lines +82 to +101
def _reduce(
self,
num_texts: int,
owner: List[int],
flat: List[str],
embeddings: List[List[float]],
) -> List[List[float]]:
out: List[List[float]] = []
for i in range(num_texts):
idxs = [j for j, o in enumerate(owner) if o == i]
if len(idxs) == 1:
out.append(embeddings[idxs[0]])
else:
out.append(
self._average(
[embeddings[j] for j in idxs],
[max(1, len(flat[j])) for j in idxs],
),
)
return out
@kesmit13
kesmit13 merged commit b0fd62b into main Aug 11, 2026
13 checks passed
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.

6 participants