mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
b17309b3f8
Add opt-in OpenAI-compatible /v1/embeddings client to eliminate duplicate in-process model loads in ruflo/ruvector co-deployments and the per-hook cold-load cost in fresh agentic-qe CLI processes. When EmbeddingConfig.endpoint (or AQE_EMBEDDER_ENDPOINT) is set, the @huggingface/transformers import is bypassed entirely and feature-extraction routes through the remote service. Endpoint unset = today's behavior. Wire format is OpenAI's /v1/embeddings so any existing embedder server (TEI, vLLM, llama.cpp, Ollama, LocalAI, LM Studio, OpenAI) works as a drop-in, not just bespoke ruvector servers. Transport is HTTP or HTTP-over-Unix-socket. Errors hard-fail with a circuit breaker (3 failures / 60s window) — no silent hash-embedding fallback, which would poison the HNSW index by mixing non-comparable vectors. A startup probe asserts dim === 384 and computes a stable identity fingerprint that catches model/quantization drift at the boundary. - src/learning/embedder-endpoint-client.ts: OpenAI client with keep-alive agent, 5s/30s timeouts, Bearer auth from env, circuit breaker, probe - src/learning/real-embeddings.ts: endpoint branch placed strictly before the dynamic transformers import so the cold-load fix holds at runtime; new isUsingEndpoint() / getEndpointIdentity() observability helpers - 23 new unit tests (real http.createServer fixtures, no http mocking) - End-to-end smoke: 9ms for import + init + probe + embed, transformers never appears in the loaded-module list Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
/**
|
|
* ADR-097 integration tests for real-embeddings.ts endpoint branch.
|
|
*
|
|
* These verify the public contract changes:
|
|
* - When `endpoint` is set, `computeRealEmbedding` / `computeBatchEmbeddings`
|
|
* route through the external service and return 384-d unit vectors
|
|
* - `isUsingEndpoint()` reports endpoint mode correctly
|
|
* - `getEndpointIdentity()` returns the fingerprint after init
|
|
* - When `endpoint` is unset, code paths are entirely unchanged (no probe,
|
|
* no fingerprint, behavior matches pre-ADR-097)
|
|
*
|
|
* The transformers-not-imported invariant is asserted indirectly by checking
|
|
* that `isUsingEndpoint()` is true and the endpoint server received the
|
|
* request. A direct module-resolver assertion would require spawning a
|
|
* subprocess; that fidelity is better verified by the build smoke test.
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import * as http from 'node:http';
|
|
import {
|
|
computeRealEmbedding,
|
|
computeBatchEmbeddings,
|
|
resetInitialization,
|
|
isUsingEndpoint,
|
|
getEndpointIdentity,
|
|
getEmbeddingDimension,
|
|
} from '../../../src/learning/real-embeddings.js';
|
|
|
|
function fakeEmbedding(text: string, dim = 384): number[] {
|
|
let seed = 0;
|
|
for (let i = 0; i < text.length; i++) seed = (seed * 31 + text.charCodeAt(i)) >>> 0;
|
|
const v = new Array<number>(dim);
|
|
let norm = 0;
|
|
for (let i = 0; i < dim; i++) {
|
|
seed = (seed * 1103515245 + 12345) >>> 0;
|
|
v[i] = ((seed >>> 16) / 32768 - 1);
|
|
norm += v[i] * v[i];
|
|
}
|
|
const k = 1 / Math.sqrt(norm);
|
|
for (let i = 0; i < dim; i++) v[i] *= k;
|
|
return v;
|
|
}
|
|
|
|
async function startServer(): Promise<{ url: string; close: () => Promise<void>; calls: number }> {
|
|
const handle = { url: '', calls: 0, close: async () => {} };
|
|
const server = http.createServer((req, res) => {
|
|
handle.calls++;
|
|
let raw = '';
|
|
req.on('data', (c) => (raw += c));
|
|
req.on('end', () => {
|
|
const { input } = JSON.parse(raw) as { input: string[] };
|
|
const data = input.map((t, i) => ({
|
|
index: i,
|
|
embedding: fakeEmbedding(t),
|
|
}));
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ data }));
|
|
});
|
|
});
|
|
await new Promise<void>((r) => server.listen(0, '127.0.0.1', r));
|
|
const addr = server.address();
|
|
handle.url = `http://127.0.0.1:${(addr as { port: number }).port}`;
|
|
handle.close = () => new Promise<void>((r) => server.close(() => r()));
|
|
return handle;
|
|
}
|
|
|
|
describe('real-embeddings.ts — ADR-097 endpoint branch', () => {
|
|
let server: Awaited<ReturnType<typeof startServer>>;
|
|
|
|
beforeEach(async () => {
|
|
resetInitialization();
|
|
server = await startServer();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
resetInitialization();
|
|
await server.close();
|
|
});
|
|
|
|
it('routes single embedding through endpoint when configured', async () => {
|
|
const vec = await computeRealEmbedding('hello world', {
|
|
endpoint: server.url,
|
|
enableCache: false,
|
|
});
|
|
expect(vec).toHaveLength(getEmbeddingDimension());
|
|
expect(isUsingEndpoint()).toBe(true);
|
|
const id = getEndpointIdentity();
|
|
expect(id).not.toBeNull();
|
|
expect(id?.fingerprint).toMatch(/^[0-9a-f]{16}$/);
|
|
expect(id?.dim).toBe(384);
|
|
// Endpoint was called at least twice: probe + actual embed
|
|
expect(server.calls).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it('routes batch embeddings through endpoint preserving order', async () => {
|
|
const inputs = ['alpha', 'beta', 'gamma', 'delta'];
|
|
const vecs = await computeBatchEmbeddings(inputs, {
|
|
endpoint: server.url,
|
|
enableCache: false,
|
|
});
|
|
expect(vecs).toHaveLength(inputs.length);
|
|
for (let i = 0; i < inputs.length; i++) {
|
|
const expected = fakeEmbedding(inputs[i]);
|
|
const dot = vecs[i].reduce((s, x, j) => s + x * expected[j], 0);
|
|
// Re-normalization preserves direction
|
|
expect(dot).toBeGreaterThan(0.999);
|
|
}
|
|
});
|
|
|
|
it('returns unit-length vectors', async () => {
|
|
const vec = await computeRealEmbedding('unit length check', {
|
|
endpoint: server.url,
|
|
enableCache: false,
|
|
});
|
|
const norm = Math.sqrt(vec.reduce((s, x) => s + x * x, 0));
|
|
expect(norm).toBeCloseTo(1, 5);
|
|
});
|
|
|
|
it('reports endpoint mode via isUsingEndpoint()', async () => {
|
|
expect(isUsingEndpoint()).toBe(false);
|
|
await computeRealEmbedding('x', { endpoint: server.url, enableCache: false });
|
|
expect(isUsingEndpoint()).toBe(true);
|
|
});
|
|
|
|
it('does not consult endpoint when endpoint config is undefined', async () => {
|
|
// We assert that NOT setting endpoint leaves isUsingEndpoint() false and never
|
|
// hits the server. The in-process path may or may not initialize depending
|
|
// on whether @huggingface/transformers is available in the test environment;
|
|
// we explicitly avoid triggering it here.
|
|
expect(server.calls).toBe(0);
|
|
expect(isUsingEndpoint()).toBe(false);
|
|
expect(getEndpointIdentity()).toBeNull();
|
|
});
|
|
|
|
it('fails loud when endpoint dim does not match', async () => {
|
|
// Replace server with one that returns wrong-dim vectors
|
|
await server.close();
|
|
const bad = http.createServer((req, res) => {
|
|
let raw = '';
|
|
req.on('data', (c) => (raw += c));
|
|
req.on('end', () => {
|
|
const { input } = JSON.parse(raw) as { input: string[] };
|
|
const data = input.map((t, i) => ({
|
|
index: i,
|
|
embedding: fakeEmbedding(t, 256), // wrong dim
|
|
}));
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ data }));
|
|
});
|
|
});
|
|
await new Promise<void>((r) => bad.listen(0, '127.0.0.1', r));
|
|
const addr = bad.address();
|
|
const url = `http://127.0.0.1:${(addr as { port: number }).port}`;
|
|
|
|
await expect(
|
|
computeRealEmbedding('x', { endpoint: url, enableCache: false })
|
|
).rejects.toThrow(/dim mismatch/);
|
|
|
|
await new Promise<void>((r) => bad.close(() => r()));
|
|
});
|
|
});
|