mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
feat(embeddings): add GITNEXUS_EMBEDDING_OMIT_DIMENSIONS for providers that reject the dimensions field
GITNEXUS_EMBEDDING_DIMS drives both the `dimensions` field sent in the request and the length the returned vector is validated against. Some OpenAI-compatible endpoints (e.g. Voyage) reject the `dimensions` field with a 400 while still returning a fixed-size vector, so they cannot be configured today: setting DIMS triggers the 400, and leaving it unset makes validation expect the 384 default and reject the real vector. Add an opt-in GITNEXUS_EMBEDDING_OMIT_DIMENSIONS flag that suppresses only the sent field. DIMS is still honoured for validation. Default behaviour is unchanged.
This commit is contained in:
parent
3b195ec100
commit
a84cbce159
3 changed files with 42 additions and 5 deletions
|
|
@ -204,6 +204,7 @@ Set these env vars to use a remote OpenAI-compatible `/v1/embeddings` endpoint i
|
|||
export GITNEXUS_EMBEDDING_URL=http://your-server:8080/v1
|
||||
export GITNEXUS_EMBEDDING_MODEL=BAAI/bge-large-en-v1.5
|
||||
export GITNEXUS_EMBEDDING_DIMS=1024 # optional, default 384
|
||||
export GITNEXUS_EMBEDDING_OMIT_DIMENSIONS=1 # optional; omit the `dimensions` field for providers that reject it (e.g. Voyage), still validating against GITNEXUS_EMBEDDING_DIMS
|
||||
export GITNEXUS_EMBEDDING_API_KEY=your-key # optional, default: "unused"
|
||||
gitnexus analyze . --embeddings
|
||||
```
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ interface HttpConfig {
|
|||
model: string;
|
||||
apiKey: string;
|
||||
dimensions?: number;
|
||||
omitDimensionsField?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,11 +51,19 @@ const readConfig = (): HttpConfig | null => {
|
|||
dimensions = parsed;
|
||||
}
|
||||
|
||||
// Some OpenAI-compatible providers (e.g. Voyage) reject the `dimensions` request field with a 400
|
||||
// yet still return a fixed-size vector. When this is set the field is not sent, while
|
||||
// GITNEXUS_EMBEDDING_DIMS is still honoured for validating the returned vector length.
|
||||
const omitDimensionsField =
|
||||
process.env.GITNEXUS_EMBEDDING_OMIT_DIMENSIONS === '1' ||
|
||||
process.env.GITNEXUS_EMBEDDING_OMIT_DIMENSIONS === 'true';
|
||||
|
||||
return {
|
||||
baseUrl: baseUrl.replace(/\/+$/, ''),
|
||||
model,
|
||||
apiKey: process.env.GITNEXUS_EMBEDDING_API_KEY ?? 'unused',
|
||||
dimensions,
|
||||
omitDimensionsField,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -96,11 +105,14 @@ interface EmbeddingItem {
|
|||
* @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
|
||||
* Matryoshka truncation (OpenAI text-embedding-3-*, Cohere embed-v3)
|
||||
* return a truncated vector at that size; endpoints that do not
|
||||
* recognise the field may ignore it or return 400. Leave
|
||||
* `GITNEXUS_EMBEDDING_DIMS` unset for strict backends that reject
|
||||
* unknown fields.
|
||||
* unknown fields, or set `GITNEXUS_EMBEDDING_OMIT_DIMENSIONS=1` to omit
|
||||
* the field while still validating the response against
|
||||
* `GITNEXUS_EMBEDDING_DIMS` (e.g. Voyage, which rejects the field but
|
||||
* always returns a fixed-size vector).
|
||||
*/
|
||||
const httpEmbedBatch = async (
|
||||
url: string,
|
||||
|
|
@ -193,7 +205,7 @@ export const httpEmbed = async (texts: string[]): Promise<Float32Array[]> => {
|
|||
config.model,
|
||||
config.apiKey,
|
||||
batchIndex,
|
||||
config.dimensions,
|
||||
config.omitDimensionsField ? undefined : config.dimensions,
|
||||
);
|
||||
|
||||
if (items.length !== batch.length) {
|
||||
|
|
@ -243,7 +255,7 @@ export const httpEmbedQuery = async (text: string): Promise<number[]> => {
|
|||
config.model,
|
||||
config.apiKey,
|
||||
0,
|
||||
config.dimensions,
|
||||
config.omitDimensionsField ? undefined : config.dimensions,
|
||||
);
|
||||
if (!items.length) {
|
||||
throw new Error(`Embedding endpoint returned empty response (${safeUrl(url)})`);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const ENV_KEYS = [
|
|||
'GITNEXUS_EMBEDDING_MODEL',
|
||||
'GITNEXUS_EMBEDDING_API_KEY',
|
||||
'GITNEXUS_EMBEDDING_DIMS',
|
||||
'GITNEXUS_EMBEDDING_OMIT_DIMENSIONS',
|
||||
] as const;
|
||||
|
||||
/** 384d mock vector matching the default schema dimensions. */
|
||||
|
|
@ -163,6 +164,29 @@ describe('HTTP embedding backend', () => {
|
|||
expect(result.length).toBe(512);
|
||||
});
|
||||
|
||||
it('omits the dimensions field on the single-query path when GITNEXUS_EMBEDDING_OMIT_DIMENSIONS is set', async () => {
|
||||
process.env.GITNEXUS_EMBEDDING_URL = 'http://test:8080/v1';
|
||||
process.env.GITNEXUS_EMBEDDING_MODEL = 'voyage-code-3';
|
||||
process.env.GITNEXUS_EMBEDDING_DIMS = '1024';
|
||||
process.env.GITNEXUS_EMBEDDING_OMIT_DIMENSIONS = '1';
|
||||
|
||||
const vec1024 = Array.from({ length: 1024 }, (_, i) => i / 1024);
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ data: [{ embedding: vec1024 }] }),
|
||||
}),
|
||||
);
|
||||
|
||||
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('dimensions' in body).toBe(false);
|
||||
expect(result.length).toBe(1024);
|
||||
});
|
||||
|
||||
it('retries on server error', async () => {
|
||||
process.env.GITNEXUS_EMBEDDING_URL = 'http://test:8080/v1';
|
||||
process.env.GITNEXUS_EMBEDDING_MODEL = 'test-model';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue