Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
- run: bunx tsc --noEmit
- run: bun run lint
- run: bun run test
- run: bun run test:e2e
- run: bunx prettier --check .
- run: bun run build
- name: Import packed dist with Node
Expand Down
18 changes: 14 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,28 @@ bun install
bun run typecheck
bun run lint
bun run test
bun run test:e2e
```

`bun run test` is hermetic. `bun run test:live` runs a `/v1/embeddings` round
trip and skips unless `EMBEDDING_E2E_BASE_URL` is set; point it at a remote
server rather than loading models locally:
`bun run test` runs the colocated units in `src/`. `bun run test:e2e` runs
`e2e/`: retry, wire and base64 cases on the `@intx/inference-testing` harness
and a local HTTP stub, plus a `/v1/embeddings` round trip that skips unless
`EMBEDDING_E2E_BASE_URL` is set; point it at a remote server rather than
loading models locally:

```bash
EMBEDDING_E2E_BASE_URL=http://100.113.184.123:11434/v1 bun run test:live
EMBEDDING_E2E_BASE_URL=http://100.113.184.123:11434/v1 bun run test:e2e
```

`EMBEDDING_E2E_MODEL` defaults to `nomic-embed-text`.

## Versioning

Semver. Releases run `bun run build && npm publish` with green CI.

## Commit messages

Commit subjects and PR titles follow [Conventional Commits](https://www.conventionalcommits.org): `feat`, `fix`, `refactor`, `test`, `docs`, `build`, `ci`, `perf`, and `chore(release): x.y.z` for releases.
Add `!` only for public API breaks: removed or renamed exports, changed signatures, newly required params. Peer and dependency range changes are `build(deps):` with no `!`.
Keep subjects imperative, lowercase after the colon, 72 characters or less, and free of ticket IDs.
Every PR links its issue with a `Closes <issue id>` line in the PR body.
20 changes: 11 additions & 9 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,16 @@ package depends only on the published surface).

## Tests

`bun run test` runs `bun test ./src ./tests`.
`bun run test` runs `bun test ./src`; `bun run test:e2e` runs `bun test ./e2e`.
Both run in CI.

- `src/embed.test.ts` — units on `@intx/inference-testing` fakes: index
placement across batches, short reply, base64 decode.
- `tests/retry.test.ts` — 429 retried once after `Retry-After` on the
harness clock; 401 rejects without retry.
- `tests/base64.test.ts` — `base64` and `float` agree over a real HTTP stub.
- `live/embeddings.test.ts` (`bun run test:live`, not in `test` or CI) —
round trip against a real `/v1/embeddings` (`EMBEDDING_E2E_BASE_URL`,
required; `EMBEDDING_E2E_MODEL`, default `nomic-embed-text`).
Skips when unset or unreachable.
placement across batches, short and duplicate replies, base64 decode,
non-JSON 200.
- `e2e/retry.test.ts` — 429 retried after `Retry-After` (seconds, HTTP-date,
custom extractor) on the harness clock; 401 rejects without retry; timeout.
- `e2e/wire.test.ts` — request URL, body fields and bearer header.
- `e2e/base64.test.ts` — `base64` and `float` agree over a real HTTP stub.
- `e2e/live.test.ts` — round trip against a real `/v1/embeddings`
(`EMBEDDING_E2E_BASE_URL`, required; `EMBEDDING_E2E_MODEL`, default
`nomic-embed-text`). Skips when unset or unreachable.
File renamed without changes.
File renamed without changes.
File renamed without changes.
57 changes: 57 additions & 0 deletions e2e/wire.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { afterEach, expect, test } from "bun:test";
import { setupHarness, type Harness } from "@intx/inference-testing";

import { embedTexts, type EmbedConfig } from "../src/index";

const CONFIG: EmbedConfig = {
baseURL: "https://embed.example/v1",
model: "text-embedding-3-small",
};

let harness: Harness | undefined;
afterEach(() => {
harness?.dispose();
harness = undefined;
});

async function run<T>(h: Harness, pending: Promise<T>): Promise<T> {
await h.run();
return await pending;
}

test("sends only configured fields to {baseURL}/embeddings", async () => {
harness = setupHarness();
const stream = harness.scenario.createStream();
harness.scenario.whenRequestBodyMatches(
(body, req) =>
req.url === "https://embed.example/v1/embeddings" &&
!req.headers.has("authorization") &&
body === JSON.stringify({ model: CONFIG.model, input: ["a"] }),
stream,
);
stream.enqueueAll(
[new TextEncoder().encode('{"data":[{"index":0,"embedding":[1]}]}')],
{ startAt: 1 },
);
const pending = embedTexts(["a"], CONFIG, { deps: harness.deps });
expect(await run(harness, pending)).toEqual([[1]]);
});

test("sends a bearer token only when apiKey is set", async () => {
harness = setupHarness();
const stream = harness.scenario.createStream();
harness.scenario.whenRequestMatches(
(req) => req.headers.get("authorization") === "Bearer sk-test",
stream,
);
stream.enqueueAll(
[new TextEncoder().encode('{"data":[{"index":0,"embedding":[1]}]}')],
{ startAt: 1 },
);
const pending = embedTexts(
["a"],
{ ...CONFIG, apiKey: "sk-test" },
{ deps: harness.deps },
);
expect(await run(harness, pending)).toEqual([[1]]);
});
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@corbits/embedding",
"description": "Embedding client for OpenAI-compatible /v1/embeddings endpoints, built on the Interchange inference primitives",
"version": "0.1.0",
"version": "0.2.0",
"license": "LGPL-2.1-only",
"author": "Sawyer Cutler <sawyer@dirtroad.dev>",
"type": "module",
Expand All @@ -18,8 +18,8 @@
"prepack": "bun run build",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"test": "bun test ./src ./tests",
"test:live": "bun test ./live",
"test": "bun test ./src",
"test:e2e": "bun test ./e2e",
"format": "prettier -w ."
},
"dependencies": {
Expand Down
53 changes: 0 additions & 53 deletions src/embed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,56 +115,3 @@ test("classifies a 200 that is not JSON instead of leaking a SyntaxError", async
reason: { category: "protocol_mismatch" },
});
});

test("sends only configured fields to {baseURL}/embeddings", async () => {
harness = setupHarness();
const stream = harness.scenario.createStream();
harness.scenario.whenRequestBodyMatches(
(body, req) =>
req.url === "https://embed.example/v1/embeddings" &&
!req.headers.has("authorization") &&
body === JSON.stringify({ model: CONFIG.model, input: ["a"] }),
stream,
);
stream.enqueueAll(
[new TextEncoder().encode('{"data":[{"index":0,"embedding":[1]}]}')],
{ startAt: 1 },
);
const pending = embedTexts(["a"], CONFIG, { deps: harness.deps });
expect(await run(harness, pending)).toEqual([[1]]);
});

test("sends a bearer token only when apiKey is set", async () => {
harness = setupHarness();
const stream = harness.scenario.createStream();
harness.scenario.whenRequestMatches(
(req) => req.headers.get("authorization") === "Bearer sk-test",
stream,
);
stream.enqueueAll(
[new TextEncoder().encode('{"data":[{"index":0,"embedding":[1]}]}')],
{ startAt: 1 },
);
const pending = embedTexts(
["a"],
{ ...CONFIG, apiKey: "sk-test" },
{ deps: harness.deps },
);
expect(await run(harness, pending)).toEqual([[1]]);
});

test("returns [] for empty input without a request", async () => {
harness = setupHarness();
const pending = embedTexts([], CONFIG, { deps: harness.deps });
expect(await run(harness, pending)).toEqual([]);
});

test("rejects a non-positive batchSize before any request", async () => {
harness = setupHarness();
for (const batchSize of [0, -1, 1.5]) {
await expect(
embedTexts(["a"], { ...CONFIG, batchSize }, { deps: harness.deps }),
).rejects.toThrow(/batchSize/);
}
await harness.run();
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@

"lib": ["ESNext"]
},
"include": ["src/**/*.ts", "tests/**/*.ts", "live/**/*.ts"]
"include": ["src/**/*.ts", "e2e/**/*.ts"]
}
Loading