mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-30 01:51:20 +00:00
fix(embeddings): send Voyage output dimensions
This commit is contained in:
parent
a4e70ec3b4
commit
c031c67f11
2 changed files with 95 additions and 7 deletions
|
|
@ -364,11 +364,10 @@ const countMismatchMessage = (
|
|||
* @param model - Model name for the request body
|
||||
* @param apiKey - Bearer token (only used in Authorization header)
|
||||
* @param batchIndex - Logical batch number (for error context)
|
||||
* @param dimensions - Optional output-vector size. When provided, sent as
|
||||
* the `dimensions` field in the request body. Endpoints that implement
|
||||
* Matryoshka truncation (OpenAI text-embedding-3-*, Cohere embed-v3,
|
||||
* Voyage) return a truncated vector at that size; endpoints that do not
|
||||
* recognise the field may ignore it or return 400. Set
|
||||
* @param dimensions - Optional output-vector size. Voyage uses its
|
||||
* `output_dimension` field; other OpenAI-compatible endpoints use
|
||||
* `dimensions`. Endpoints that do not recognise the field may ignore it or
|
||||
* return 400. Set
|
||||
* `GITNEXUS_EMBEDDING_REQUEST_DIMS=omit` for strict backends while keeping
|
||||
* `GITNEXUS_EMBEDDING_DIMS` set to the returned vector size.
|
||||
*/
|
||||
|
|
@ -385,12 +384,27 @@ const httpEmbedBatch = async (
|
|||
minIntervalMs = 0,
|
||||
timeoutMs = DEFAULT_HTTP_TIMEOUT_MS,
|
||||
): Promise<EmbeddingItem[]> => {
|
||||
const requestBody: { input: string[]; model: string; dimensions?: number } = {
|
||||
const requestBody: {
|
||||
input: string[];
|
||||
model: string;
|
||||
dimensions?: number;
|
||||
output_dimension?: number;
|
||||
} = {
|
||||
input: batch,
|
||||
model,
|
||||
};
|
||||
if (dimensions !== undefined) {
|
||||
requestBody.dimensions = dimensions;
|
||||
let hostname = '';
|
||||
try {
|
||||
hostname = new URL(url).hostname.toLowerCase().replace(/\.$/, '');
|
||||
} catch {
|
||||
// Fetch below owns malformed-URL reporting.
|
||||
}
|
||||
if (hostname === 'voyageai.com' || hostname.endsWith('.voyageai.com')) {
|
||||
requestBody.output_dimension = dimensions;
|
||||
} else {
|
||||
requestBody.dimensions = dimensions;
|
||||
}
|
||||
}
|
||||
|
||||
// Built on demand, not up front. Both describe faults, so in a healthy run —
|
||||
|
|
|
|||
|
|
@ -168,6 +168,56 @@ describe('HTTP embedding backend', () => {
|
|||
expect(result.length).toBe(1024);
|
||||
});
|
||||
|
||||
it('uses output_dimension for Voyage document batches', async () => {
|
||||
process.env.GITNEXUS_EMBEDDING_URL = 'https://api.voyageai.com/v1';
|
||||
process.env.GITNEXUS_EMBEDDING_MODEL = 'voyage-code-3';
|
||||
process.env.GITNEXUS_EMBEDDING_API_KEY = 'test-key';
|
||||
process.env.GITNEXUS_EMBEDDING_DIMS = '2048';
|
||||
|
||||
const vec2048 = Array.from({ length: 2048 }, (_, i) => i / 2048);
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ data: [{ embedding: vec2048 }] }),
|
||||
}),
|
||||
);
|
||||
|
||||
const { embedText } = await import('../../src/core/embeddings/embedder.js');
|
||||
const result = await embedText('test text');
|
||||
|
||||
const body = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(body).toMatchObject({
|
||||
input: ['test text'],
|
||||
model: 'voyage-code-3',
|
||||
output_dimension: 2048,
|
||||
});
|
||||
expect(body.dimensions).toBeUndefined();
|
||||
expect(result.length).toBe(2048);
|
||||
});
|
||||
|
||||
it('does not treat a voyageai.com lookalike host as Voyage', async () => {
|
||||
process.env.GITNEXUS_EMBEDDING_URL = 'https://voyageai.com.example/v1';
|
||||
process.env.GITNEXUS_EMBEDDING_MODEL = 'test-model';
|
||||
process.env.GITNEXUS_EMBEDDING_DIMS = '512';
|
||||
|
||||
const vec512 = Array.from({ length: 512 }, (_, i) => i / 512);
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ data: [{ embedding: vec512 }] }),
|
||||
}),
|
||||
);
|
||||
|
||||
const { embedText } = await import('../../src/core/embeddings/embedder.js');
|
||||
await embedText('test text');
|
||||
|
||||
const body = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(body.dimensions).toBe(512);
|
||||
expect(body.output_dimension).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can validate custom dims without forwarding dimensions to strict backends', async () => {
|
||||
process.env.GITNEXUS_EMBEDDING_URL = 'http://test:8080/v1';
|
||||
process.env.GITNEXUS_EMBEDDING_MODEL = 'bge-m3';
|
||||
|
|
@ -214,6 +264,30 @@ describe('HTTP embedding backend', () => {
|
|||
expect(result.length).toBe(512);
|
||||
});
|
||||
|
||||
it('uses output_dimension for Voyage on the single-query path', async () => {
|
||||
process.env.GITNEXUS_EMBEDDING_URL = 'https://voyageai.com/v1';
|
||||
process.env.GITNEXUS_EMBEDDING_MODEL = 'voyage-code-3';
|
||||
process.env.GITNEXUS_EMBEDDING_API_KEY = 'test-key';
|
||||
process.env.GITNEXUS_EMBEDDING_DIMS = '2048';
|
||||
|
||||
const vec2048 = Array.from({ length: 2048 }, (_, i) => i / 2048);
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ data: [{ embedding: vec2048 }] }),
|
||||
}),
|
||||
);
|
||||
|
||||
const mod = await import('../../src/mcp/core/embedder.js');
|
||||
const result = await mod.embedQuery('query text');
|
||||
|
||||
const body = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(body.output_dimension).toBe(2048);
|
||||
expect(body.dimensions).toBeUndefined();
|
||||
expect(result.length).toBe(2048);
|
||||
});
|
||||
|
||||
it('can omit dimensions on the single-query path while validating custom dims', async () => {
|
||||
process.env.GITNEXUS_EMBEDDING_URL = 'http://test:8080/v1';
|
||||
process.env.GITNEXUS_EMBEDDING_MODEL = 'bge-m3';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue