Repository navigation
fix(ai-openai): split embed() inputs into requests of at most 2048 #1986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ded6574
384f3fb
a1688ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/ai-openai': patch | ||
| --- | ||
|
|
||
| `embed()` with the OpenAI adapter now sends inputs in sequential requests of at most 2048 inputs each, the endpoint's limit, and sums the token usage. Before, more than 2048 texts failed with a 400 error. OpenAI also limits one request to 300,000 tokens across all inputs, so a large array of long texts can still fail. Split those calls yourself. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,14 +24,23 @@ import type { OpenAIClientConfig } from '../utils/client' | |
| */ | ||
| export interface OpenAIEmbeddingConfig extends OpenAIClientConfig {} | ||
|
|
||
| /** | ||
| * The embeddings endpoint rejects requests with more than 2048 inputs, so | ||
| * larger arrays are sent as several sequential requests. | ||
| */ | ||
| // ponytail: splits by input count only. OpenAI also caps a request at 300k | ||
| // tokens summed across inputs, so 2048 long texts can still 400. Add a | ||
| // token-budget split if that bites (needs a tokeniser or char estimate). | ||
| const OPENAI_MAX_BATCH_SIZE = 2048 | ||
|
|
||
| /** | ||
| * OpenAI Embedding Adapter | ||
| * | ||
| * Tree-shakeable adapter for OpenAI text embeddings. | ||
| * Supports text-embedding-3-small and text-embedding-3-large. | ||
| * | ||
| * Features: | ||
| * - Batch embedding (one request for the whole input array) | ||
| * - Batch embedding (sequential requests of at most 2048 inputs each; usage is summed) | ||
| * - Matryoshka dimension reduction via the top-level `dimensions` option | ||
| */ | ||
| export class OpenAIEmbeddingAdapter< | ||
|
|
@@ -62,40 +71,59 @@ export class OpenAIEmbeddingAdapter< | |
| // fields (server routes often pass modelOptions through from untyped | ||
| // client input). encoding_format is pinned to float so vectors are | ||
| // always number[]. | ||
| const request: OpenAI_SDK.EmbeddingCreateParams = { | ||
| const baseRequest = { | ||
| ...modelOptions, | ||
| model, | ||
| input: texts, | ||
| encoding_format: 'float', | ||
| } | ||
| if (options.dimensions !== undefined) { | ||
| request.dimensions = options.dimensions | ||
| } | ||
| } as const | ||
| const dimensions = | ||
| options.dimensions !== undefined | ||
| ? { dimensions: options.dimensions } | ||
| : {} | ||
|
|
||
| logger.request( | ||
| `activity=embed provider=${this.name} model=${model} inputs=${texts.length}`, | ||
| { provider: this.name, model }, | ||
| ) | ||
|
|
||
| const response = options.abortSignal | ||
| ? await this.client.embeddings.create(request, { | ||
| signal: options.abortSignal, | ||
| }) | ||
| : await this.client.embeddings.create(request) | ||
| const embeddings: EmbeddingResult['embeddings'] = [] | ||
| let promptTokens = 0 | ||
| let totalTokens = 0 | ||
|
|
||
| for ( | ||
| let start = 0; | ||
| start < texts.length; | ||
| start += OPENAI_MAX_BATCH_SIZE | ||
| ) { | ||
| const request: OpenAI_SDK.EmbeddingCreateParams = { | ||
| ...baseRequest, | ||
| ...dimensions, | ||
| input: texts.slice(start, start + OPENAI_MAX_BATCH_SIZE), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| const response = options.abortSignal | ||
| ? await this.client.embeddings.create(request, { | ||
| signal: options.abortSignal, | ||
| }) | ||
| : await this.client.embeddings.create(request) | ||
|
|
||
| for (const item of response.data) { | ||
| embeddings.push({ vector: item.embedding, index: start + item.index }) | ||
| } | ||
| promptTokens += response.usage.prompt_tokens | ||
| totalTokens += response.usage.total_tokens | ||
| } | ||
|
|
||
| const usage: TokenUsage = { | ||
| promptTokens: response.usage.prompt_tokens, | ||
| promptTokens, | ||
| completionTokens: 0, | ||
| totalTokens: response.usage.total_tokens, | ||
| totalTokens, | ||
| } | ||
|
|
||
| return { | ||
| id: generateId(this.name), | ||
| model, | ||
| embeddings: response.data.map((item) => ({ | ||
| vector: item.embedding, | ||
| index: item.index, | ||
| })), | ||
| embeddings, | ||
| usage, | ||
| } | ||
| } catch (error: unknown) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: TanStack/ai
Length of output: 2207
Script executed:
Repository: TanStack/ai
Length of output: 22343
Script executed:
Repository: TanStack/ai
Length of output: 7457
Script executed:
Repository: TanStack/ai
Length of output: 4565
Script executed:
Repository: TanStack/ai
Length of output: 806
Web query:
OpenAI embeddings API create input parameter empty array error documentationResult:
Reject empty input in
createEmbeddingsbefore the batch loop.With
input: [], the new loop makes no request and returnsembeddings: []with zero usage. Before this change, the API rejected the empty array with a 400 error. Throw before the loop so an empty input reports an error, as the OpenAI endpoint and sibling activities do.Suggested fix