Skip to content

Commit 394aa79

Browse files
Address pre-publish review findings for corbits-embedding (#1)
* Address pre-publish review findings for corbits-embedding Reject batchSize below 1 at the embedTexts entry boundary, document installation and transport exports, and complete the publish metadata. * docs: install from git until @corbits/embedding is on npm npm view @corbits/embedding 404s; git is the install path. Default export is dist/; native Node does not load extensionless TypeScript source. * fix: emit .js specifiers in dist and integer batchSize Node cannot import extensionless relative specifiers from packed dist. Source relative imports now carry .js so tsc emits Node-loadable ESM. EmbedConfigSchema and embedTexts both require batchSize integer >= 1, rejecting 0.5 and NaN.
1 parent f3d9557 commit 394aa79

5 files changed

Lines changed: 89 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ from an embedding endpoint therefore behaves exactly as one from a chat
2222
endpoint, and a `credential_failure` aborts immediately rather than retrying a
2323
bad key.
2424

25+
## Installation
26+
27+
Not published to npm yet. Until a registry publish, `npm install @corbits/embedding`
28+
404s. Git is the install path.
29+
30+
```bash
31+
bun add github:corbitsdev/corbits-embedding
32+
```
33+
34+
Requires Node >= 20 or Bun >= 1.2. The default export is built `dist/`; native Node
35+
does not load this package's extensionless TypeScript source.
36+
2537
```ts
2638
import { createDefaultScheduler } from "@intx/inference";
2739
import { embedTexts, probeEmbedDims } from "@corbits/embedding";
@@ -59,6 +71,19 @@ copy of this class until the shared transport is upstreamed, so `instanceof`
5971
does not hold across the two. Catching both? Discriminate on
6072
`error.name === "ModelRequestError"`.
6173

74+
## Transport exports
75+
76+
The barrel re-exports the transport `embedTexts` is built on, for sibling
77+
one-shot JSON clients that want the same classified, retried request path
78+
without touching `@intx/inference` internals:
79+
80+
- `runJSONRequest` — one JSON POST with error classification and retry.
81+
- `extractRetryAfterMs` — the default `Retry-After` reader, overridable per
82+
call via `extractRetryAfterMs`.
83+
- `ModelRequestError` — the error every failure mode raises, carrying the
84+
classified `InferenceError` as `reason` and the URL.
85+
- Types `RunRequestOptions` and `RetryAfterExtractor`.
86+
6287
## Versioning
6388

6489
Semver. Released manually with `bun run build && npm publish` once CI is green.

‎package.json‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"description": "Embedding client for OpenAI-compatible /v1/embeddings endpoints, built on the Interchange inference primitives",
44
"version": "0.1.0",
55
"license": "LGPL-2.1-only",
6+
"author": "Sawyer Cutler <sawyer@dirtroad.dev>",
67
"type": "module",
78
"exports": {
89
".": {
@@ -29,7 +30,11 @@
2930
"prettier": "3.6.2",
3031
"typescript": "5.9.3"
3132
},
32-
"files": ["dist", "README.md", "LICENSE"],
33+
"files": [
34+
"dist",
35+
"README.md",
36+
"LICENSE"
37+
],
3338
"sideEffects": false,
3439
"publishConfig": {
3540
"access": "public"
@@ -38,7 +43,12 @@
3843
"type": "git",
3944
"url": "git+https://github.com/corbitsdev/corbits-embedding.git"
4045
},
46+
"homepage": "https://github.com/corbitsdev/corbits-embedding",
47+
"bugs": {
48+
"url": "https://github.com/corbitsdev/corbits-embedding/issues"
49+
},
4150
"engines": {
42-
"bun": ">=1.2.0"
51+
"bun": ">=1.2.0",
52+
"node": ">=20"
4353
}
4454
}

‎src/embed.test.ts‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import { type } from "arktype";
12
import { describe, expect, it } from "bun:test";
23
import type { Dependencies } from "@intx/inference";
34

4-
import { embedTexts, probeEmbedDims, type EmbedConfig } from "./embed";
5+
import {
6+
embedTexts,
7+
probeEmbedDims,
8+
EmbedConfigSchema,
9+
type EmbedConfig,
10+
} from "./embed";
511
import { extractRetryAfterMs, ModelRequestError } from "./request";
612

713
const CONFIG: EmbedConfig = {
@@ -125,6 +131,19 @@ describe("embedTexts", () => {
125131
expect(vectors.map((v) => v[0])).toEqual([0, 1, 0]);
126132
});
127133

134+
it("rejects a batchSize that is not an integer >= 1 instead of looping forever", async () => {
135+
// `batches` advances by `i += size`, so a size of 0 never advances and
136+
// grows the batch list until OOM. Fractional sizes would also stall or
137+
// slice wrongly. Schema and runtime both require integer >= 1.
138+
for (const batchSize of [0, -1, 0.5, Number.NaN, 1.5]) {
139+
const { deps: d, calls } = deps([embedding(3)]);
140+
await expect(
141+
embedTexts(["a"], { ...CONFIG, batchSize }, { deps: d }),
142+
).rejects.toBeInstanceOf(RangeError);
143+
expect(calls).toHaveLength(0);
144+
}
145+
});
146+
128147
it("rejects a malformed response rather than returning junk vectors", async () => {
129148
const { deps: d } = deps([
130149
Response.json({ data: [{ index: 0, embedding: true }] }),
@@ -135,6 +154,26 @@ describe("embedTexts", () => {
135154
});
136155
});
137156

157+
describe("EmbedConfigSchema", () => {
158+
const base = { baseURL: "https://embed.example/v1", model: "m" };
159+
160+
it("accepts an integer batchSize >= 1 and an omitted batchSize", () => {
161+
expect(EmbedConfigSchema({ ...base, batchSize: 1 })).toEqual({
162+
...base,
163+
batchSize: 1,
164+
});
165+
expect(EmbedConfigSchema(base)).toEqual(base);
166+
});
167+
168+
it("rejects a fractional, non-integer, or sub-1 batchSize", () => {
169+
for (const batchSize of [0, -1, 0.5, Number.NaN, 1.5]) {
170+
expect(EmbedConfigSchema({ ...base, batchSize })).toBeInstanceOf(
171+
type.errors,
172+
);
173+
}
174+
});
175+
});
176+
138177
describe("retry behaviour inherited from the inference policy", () => {
139178
it("retries a 429 and succeeds on the follow-up", async () => {
140179
// The hand-rolled client this replaced had no retry at all: a single 429

‎src/embed.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
runJSONRequest,
77
type RequestDependencies,
88
type RetryAfterExtractor,
9-
} from "./request";
9+
} from "./request.js";
1010

1111
/**
1212
* `/v1/embeddings` is the one wire format worth supporting — OpenAI, Ollama,
@@ -30,7 +30,7 @@ export const EmbedConfigSchema = type({
3030
* Inputs per request. Providers cap this — OpenAI rejects arrays over 2048 —
3131
* and a smaller batch is also how you stay under a per-request token ceiling.
3232
*/
33-
"batchSize?": "number > 0",
33+
"batchSize?": "number.integer >= 1",
3434
"timeoutMs?": "number > 0",
3535
});
3636
export type EmbedConfig = typeof EmbedConfigSchema.infer;
@@ -165,8 +165,15 @@ export async function embedTexts(
165165
): Promise<number[][]> {
166166
if (texts.length === 0) return [];
167167

168+
const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
169+
if (!Number.isInteger(batchSize) || batchSize < 1) {
170+
throw new RangeError(
171+
`batchSize must be an integer >= 1, got ${batchSize}: a batch size below 1 would never advance through the input`,
172+
);
173+
}
174+
168175
const vectors: number[][] = [];
169-
for (const batch of batches(texts, config.batchSize ?? DEFAULT_BATCH_SIZE)) {
176+
for (const batch of batches(texts, batchSize)) {
170177
const request = buildRequest(config, batch);
171178
const body = await runJSONRequest(request, {
172179
deps: options.deps,

‎src/index.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ export {
44
EmbedConfigSchema,
55
type EmbedConfig,
66
type EmbedOptions,
7-
} from "./embed";
7+
} from "./embed.js";
88
export {
99
runJSONRequest,
1010
extractRetryAfterMs,
1111
ModelRequestError,
1212
type RetryAfterExtractor,
1313
type RunRequestOptions,
14-
} from "./request";
14+
} from "./request.js";

0 commit comments

Comments
 (0)