From 32a619a1fc1396af7a4431a52fb1cf02abdb7aac Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 21 Sep 2026 21:09:52 -0700 Subject: [PATCH 1/2] docs(readme): Quickstart shows real host usage (CL-8775) --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++--------- bun.lock | 11 +++++++++ package.json | 1 + 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d269fa3..b4d4a06 100644 --- a/README.md +++ b/README.md @@ -20,22 +20,70 @@ Runs on Bun >= 1.2 or Node >= 24. The built `dist/` entry is the default import. ## Quickstart +This package never touches credential storage — it takes an already-resolved +`apiKey`. A host wraps `embedTexts` in a function of its own that resolves the +key first: an operator-set env var if present, otherwise the tenant's own +stored credential, decrypted through the host's `CredentialCipher`. That +function, not `embedTexts` directly, is what a hub route or ingestion worker +calls. + ```ts +import type { DB } from "@intx/db"; +import { resolveCredentialByName } from "@intx/db"; import { createDefaultScheduler } from "@intx/inference"; -import { embedTexts, probeEmbedDims } from "@corbits/embedding"; - -const deps = { fetch, scheduler: createDefaultScheduler() }; - -const config = { - baseURL: "http://localhost:11434/v1", // provider root, version prefix included - model: "nomic-embed-text", +import type { CredentialCipher } from "@intx/types"; +import { credentialAad } from "@intx/types"; +import { embedTexts, type EmbedConfig } from "@corbits/embedding"; + +export type TenantEmbedHost = { + db: DB["db"]; + credentialCipher: CredentialCipher; + tenantId: string; + baseURL: string; + model: string; }; -const dims = await probeEmbedDims(config, { deps }); -const [vector] = await embedTexts(["hello world"], config, { deps }); +// An operator-set EMBED_API_KEY wins outright -- e.g. a shared local +// endpoint with no per-tenant credential at all. Otherwise fall back to the +// tenant's own stored credential, named "embedding". +async function resolveEmbedApiKey( + host: TenantEmbedHost, +): Promise { + const envKey = process.env["EMBED_API_KEY"]; + if (envKey !== undefined) return envKey; + + const credential = await resolveCredentialByName( + host.db, + host.tenantId, + "embedding", + ); + if (credential === null) return undefined; + + return host.credentialCipher.decrypt( + credential.secret, + credentialAad(credential.id, "secret"), + ); +} + +export async function embedForTenant( + texts: readonly string[], + host: TenantEmbedHost, +): Promise { + const apiKey = await resolveEmbedApiKey(host); + const config: EmbedConfig = { + baseURL: host.baseURL, + model: host.model, + ...(apiKey !== undefined ? { apiKey } : {}), + }; + const deps = { fetch, scheduler: createDefaultScheduler() }; + return embedTexts(texts, config, { deps }); +} ``` -`embedTexts(texts, config, options)` batches sequentially, posts each batch to `{baseURL}/embeddings`, and returns vectors in input order. Options carry `deps`, with optional `retryPolicy`, `extractRetryAfterMs`, and `signal`. +`embedTexts(texts, config, options)` batches sequentially, posts each batch to +`{baseURL}/embeddings`, and returns vectors in input order. `options` carries +`deps` (a host's real `fetch` plus `createDefaultScheduler()`, the production +scheduler), with optional `retryPolicy`, `extractRetryAfterMs`, and `signal`. ## Dimensionality @@ -45,7 +93,7 @@ const [vector] = await embedTexts(["hello world"], config, { deps }); Every failure mode — transport, HTTP status, or a 200 with an unexpected body — raises `ModelRequestError`, carrying the classified `InferenceError` as `reason` plus the request URL. The embedding and reranking packages each carry their own copy of this class while the shared transport is upstreamed, so code catching both discriminates on `error.name === "ModelRequestError"`. -## Transport exports +## Lower-level: transport The barrel also re-exports the one-shot JSON transport `embedTexts` is built on, for sibling clients that want the same classified, retried request path: diff --git a/bun.lock b/bun.lock index 6b397c1..a30b7d8 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "arktype": "^2.1.29", }, "devDependencies": { + "@intx/db": "^0.3.0", "@intx/inference-testing": "0.3.0", "@types/bun": "1.3.9", "prettier": "3.6.2", @@ -23,6 +24,12 @@ "@ark/util": ["@ark/util@0.56.2", "", {}, "sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ=="], + "@intx/authz": ["@intx/authz@0.3.0", "", { "dependencies": { "@intx/types": "0.3.0" } }, "sha512-kyIKQAVjfB8H84mES0QA4cNnYfpPoyLmQITVBxiuXv1VO/K74TQZnCDpoo0IL7oFk2q7bbDXSAEE4ubMBVTHzA=="], + + "@intx/crypto": ["@intx/crypto@0.3.0", "", { "dependencies": { "@intx/types": "0.3.0" } }, "sha512-NsRzvkFGb0Pcsm9uLFFSXNl6Cbu+ii6VkYAw81qs9IbZzbAFfsZSUVQaX6EZuPNyJ8KK4KmBtqmioq1mhXsJww=="], + + "@intx/db": ["@intx/db@0.3.0", "", { "dependencies": { "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", "@intx/log": "0.3.0", "@intx/types": "0.3.0", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "postgres": "^3.4.8" } }, "sha512-MXNeUVJTftQuSHj5zSUBQsUcFN3wUBs8t6yT6lqjbaNxpT1C7kGx8qXb95WuadZln+krlLhyoUwGMF7EmYvlnw=="], + "@intx/inference": ["@intx/inference@0.3.0", "", { "dependencies": { "@intx/log": "0.3.0", "@intx/types": "0.3.0", "arktype": "^2.1.29" } }, "sha512-2DxqWRp5cSziXgDXPomSy0HTLVesBCtZErsKXYFnliTwYM6U+GRKk5rzwWMMgMWMFbtUaiogPExcNQYBYmfjQA=="], "@intx/inference-discovery": ["@intx/inference-discovery@0.3.0", "", { "dependencies": { "@intx/types": "0.3.0", "arktype": "^2.1.29" } }, "sha512-JdlwqTuF5z53Hfy36ANZVrbCjhFoqHgJiVNyV/LSNpTA/sPGFxutuPB1MRG5gTtDpLE0tJV775NiRXGqp+F3zA=="], @@ -47,8 +54,12 @@ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "drizzle-orm": ["drizzle-orm@0.45.3", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@netlify/db": ">=0.4.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@netlify/db", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-CAloER21cDdcgZ1OmjrZX82EeRNsdP+y0onX/eyoYDaYzki/adrAa91lzM5jWh7zZPK4qUbw/6LGv9J+A+uizA=="], + "hono": ["hono@4.12.32", "", {}, "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg=="], + "postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], diff --git a/package.json b/package.json index 3e61cfd..a080817 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "arktype": "^2.1.29" }, "devDependencies": { + "@intx/db": "^0.3.0", "@intx/inference-testing": "0.3.0", "@types/bun": "1.3.9", "prettier": "3.6.2", From fe502a7f0a018224ded2fd4a087a0f5691fd0463 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 21 Sep 2026 21:12:17 -0700 Subject: [PATCH 2/2] docs(readme): host-supplied EmbedConfig, no invented credential lookup (CL-8775) --- README.md | 80 ++++++++++++++++++++-------------------------------- bun.lock | 11 -------- package.json | 1 - 3 files changed, 31 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index b4d4a06..07a4c52 100644 --- a/README.md +++ b/README.md @@ -20,64 +20,46 @@ Runs on Bun >= 1.2 or Node >= 24. The built `dist/` entry is the default import. ## Quickstart -This package never touches credential storage — it takes an already-resolved -`apiKey`. A host wraps `embedTexts` in a function of its own that resolves the -key first: an operator-set env var if present, otherwise the tenant's own -stored credential, decrypted through the host's `CredentialCipher`. That -function, not `embedTexts` directly, is what a hub route or ingestion worker -calls. +This package never touches credential storage or config sources — it takes an +already-built `EmbedConfig`. Where that config comes from is the host's +choice; `@corbits/memory`, for example, reads it straight from +`EMBED_BASE_URL`/`EMBED_MODEL`/`EMBED_API_KEY` env vars, all-or-nothing on the +first two. A host wraps `embedTexts` in a function of its own that builds the +config once (at startup, or per call) and probes dimensionality up front so a +later model swap is caught as a migration, not a silent width mismatch. ```ts -import type { DB } from "@intx/db"; -import { resolveCredentialByName } from "@intx/db"; import { createDefaultScheduler } from "@intx/inference"; -import type { CredentialCipher } from "@intx/types"; -import { credentialAad } from "@intx/types"; -import { embedTexts, type EmbedConfig } from "@corbits/embedding"; - -export type TenantEmbedHost = { - db: DB["db"]; - credentialCipher: CredentialCipher; - tenantId: string; - baseURL: string; - model: string; -}; - -// An operator-set EMBED_API_KEY wins outright -- e.g. a shared local -// endpoint with no per-tenant credential at all. Otherwise fall back to the -// tenant's own stored credential, named "embedding". -async function resolveEmbedApiKey( - host: TenantEmbedHost, -): Promise { - const envKey = process.env["EMBED_API_KEY"]; - if (envKey !== undefined) return envKey; - - const credential = await resolveCredentialByName( - host.db, - host.tenantId, - "embedding", - ); - if (credential === null) return undefined; - - return host.credentialCipher.decrypt( - credential.secret, - credentialAad(credential.id, "secret"), - ); +import { + embedTexts, + probeEmbedDims, + type EmbedConfig, +} from "@corbits/embedding"; + +export function loadEmbedConfig(): EmbedConfig | undefined { + const baseURL = process.env["EMBED_BASE_URL"]; + const model = process.env["EMBED_MODEL"]; + if (baseURL === undefined || model === undefined) return undefined; + + const apiKey = process.env["EMBED_API_KEY"]; + return { baseURL, model, ...(apiKey !== undefined ? { apiKey } : {}) }; } -export async function embedForTenant( +export async function embed( texts: readonly string[], - host: TenantEmbedHost, + config: EmbedConfig, ): Promise { - const apiKey = await resolveEmbedApiKey(host); - const config: EmbedConfig = { - baseURL: host.baseURL, - model: host.model, - ...(apiKey !== undefined ? { apiKey } : {}), - }; const deps = { fetch, scheduler: createDefaultScheduler() }; return embedTexts(texts, config, { deps }); } + +// At startup: fail fast if the configured model's width doesn't match +// what's already stored. +const config = loadEmbedConfig(); +if (config !== undefined) { + const deps = { fetch, scheduler: createDefaultScheduler() }; + const dims = await probeEmbedDims(config, { deps }); +} ``` `embedTexts(texts, config, options)` batches sequentially, posts each batch to @@ -104,7 +86,7 @@ The barrel also re-exports the one-shot JSON transport `embedTexts` is built on, ## Interchange -Interchange hubs and ingestion workers use this package for embeddings behind the shared `@intx/inference` transport: same fetch path, same error taxonomy, and same retry behavior as chat. Memory and retrieval pipelines pair `probeEmbedDims` at startup with `embedTexts` batches at ingest and query time. +Built on the shared `@intx/inference` transport, so an embeddings call fails and retries the same way a chat call does: same `deps.fetch` path, same `InferenceError` classification, same `createDefaultRetryPolicy` backoff. A retrieval pipeline typically pairs `probeEmbedDims` at startup with `embedTexts` batches at ingest and query time, as described above. ## Versioning diff --git a/bun.lock b/bun.lock index a30b7d8..6b397c1 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,6 @@ "arktype": "^2.1.29", }, "devDependencies": { - "@intx/db": "^0.3.0", "@intx/inference-testing": "0.3.0", "@types/bun": "1.3.9", "prettier": "3.6.2", @@ -24,12 +23,6 @@ "@ark/util": ["@ark/util@0.56.2", "", {}, "sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ=="], - "@intx/authz": ["@intx/authz@0.3.0", "", { "dependencies": { "@intx/types": "0.3.0" } }, "sha512-kyIKQAVjfB8H84mES0QA4cNnYfpPoyLmQITVBxiuXv1VO/K74TQZnCDpoo0IL7oFk2q7bbDXSAEE4ubMBVTHzA=="], - - "@intx/crypto": ["@intx/crypto@0.3.0", "", { "dependencies": { "@intx/types": "0.3.0" } }, "sha512-NsRzvkFGb0Pcsm9uLFFSXNl6Cbu+ii6VkYAw81qs9IbZzbAFfsZSUVQaX6EZuPNyJ8KK4KmBtqmioq1mhXsJww=="], - - "@intx/db": ["@intx/db@0.3.0", "", { "dependencies": { "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", "@intx/log": "0.3.0", "@intx/types": "0.3.0", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "postgres": "^3.4.8" } }, "sha512-MXNeUVJTftQuSHj5zSUBQsUcFN3wUBs8t6yT6lqjbaNxpT1C7kGx8qXb95WuadZln+krlLhyoUwGMF7EmYvlnw=="], - "@intx/inference": ["@intx/inference@0.3.0", "", { "dependencies": { "@intx/log": "0.3.0", "@intx/types": "0.3.0", "arktype": "^2.1.29" } }, "sha512-2DxqWRp5cSziXgDXPomSy0HTLVesBCtZErsKXYFnliTwYM6U+GRKk5rzwWMMgMWMFbtUaiogPExcNQYBYmfjQA=="], "@intx/inference-discovery": ["@intx/inference-discovery@0.3.0", "", { "dependencies": { "@intx/types": "0.3.0", "arktype": "^2.1.29" } }, "sha512-JdlwqTuF5z53Hfy36ANZVrbCjhFoqHgJiVNyV/LSNpTA/sPGFxutuPB1MRG5gTtDpLE0tJV775NiRXGqp+F3zA=="], @@ -54,12 +47,8 @@ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], - "drizzle-orm": ["drizzle-orm@0.45.3", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@netlify/db": ">=0.4.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@netlify/db", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-CAloER21cDdcgZ1OmjrZX82EeRNsdP+y0onX/eyoYDaYzki/adrAa91lzM5jWh7zZPK4qUbw/6LGv9J+A+uizA=="], - "hono": ["hono@4.12.32", "", {}, "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg=="], - "postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], - "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], diff --git a/package.json b/package.json index a080817..3e61cfd 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "arktype": "^2.1.29" }, "devDependencies": { - "@intx/db": "^0.3.0", "@intx/inference-testing": "0.3.0", "@types/bun": "1.3.9", "prettier": "3.6.2",