byo-sdk
v0.4.0
Published
BYO - TypeScript/Node.js SDK for BYOK
Maintainers
Readme
BYO TS/JS SDK for BYOK
Official TypeScript/Node.js SDK for BYO — BYOK (Bring Your Own Key).
Installation
npm install byo-sdkQuick Start
import { BYOK } from 'byo-sdk';
const byok = new BYOK({
apiKey: process.env.BYOK_API_KEY!,
// baseURL defaults to BYO_BASE_URL env var, or http://localhost:3000 for local dev
});
// Proxy an OpenAI request using a customer's stored key
const openai = byok.openai({ refId: 'customer_123' });
const response = await openai.responses.create({
model: 'gpt-4.1',
input: 'Hello, world!',
});
console.log(response);OpenAI
Responses API
const openai = byok.openai({ refId: 'customer_123' });
const response = await openai.responses.create({
model: 'gpt-4.1',
input: 'What is BYOK?',
});Chat Completions API
const openai = byok.openai({ refId: 'customer_123' });
const response = await openai.chat.completions.create({
model: 'gpt-4.1',
messages: [{ role: 'user', content: 'Hello!' }],
});Custom OpenAI-compatible Endpoint
Customers can connect a key for any OpenAI-compatible server (vLLM, OpenRouter, Ollama, Together AI, etc.) by providing a baseUrl in providerConfig when connecting:
await byok.keys.connect({
provider: 'openai',
refId: 'customer_123',
providerKey: 'key-...',
providerConfig: { baseUrl: 'https://openrouter.ai/api/v1' },
});Once connected, proxy calls work the same way — BYO routes to the custom endpoint automatically:
const openai = byok.openai({ refId: 'customer_123' });
const response = await openai.chat.completions.create({
model: 'meta-llama/llama-3-70b',
messages: [{ role: 'user', content: 'Hello!' }],
});Anthropic
Messages API
const claude = byok.anthropic({ refId: 'customer_123' });
const response = await claude.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello!' }],
});Google AI Studio
Generate Content
const gemini = byok.google({ refId: 'customer_123' });
const response = await gemini.generateContent.create({
model: 'gemini-2.0-flash',
contents: [{ parts: [{ text: 'Hello!' }] }],
});Azure OpenAI
Chat Completions
const azure = byok.azureOpenai({ refId: 'customer_123' });
const response = await azure.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello!' }],
});Azure OpenAI requires providerConfig when connecting the key:
await byok.keys.connect({
provider: 'azure-openai',
refId: 'customer_123',
providerKey: 'your-azure-api-key',
providerConfig: {
baseUrl: 'https://your-resource.openai.azure.com',
deploymentName: 'gpt-4',
},
});AWS Bedrock
Converse
const bedrock = byok.bedrock({ refId: 'customer_123' });
const response = await bedrock.converse.create({
modelId: 'anthropic.claude-3-haiku-20240307-v1:0',
messages: [{ role: 'user', content: [{ text: 'Hello!' }] }],
});AWS Bedrock requires providerConfig when connecting the key:
await byok.keys.connect({
provider: 'bedrock',
refId: 'customer_123',
providerKey: 'your-aws-secret-access-key',
providerConfig: {
accessKeyId: 'AKIA...',
region: 'us-east-1',
},
});Key Management
// Connect a provider key (typically called from customer frontend → your API → BYO)
await byok.keys.connect({
provider: 'openai',
refId: 'customer_123',
providerKey: 'sk-...',
});
// Validate a stored key
const result = await byok.keys.validate({
provider: 'openai',
refId: 'customer_123',
});
// Revoke a stored key
await byok.keys.revoke({
provider: 'openai',
refId: 'customer_123',
});Usage and cost
Every proxied LLM call captures the model, prompt/completion tokens and an
estimated USD cost. Read it back per customer (refId) to power your own
billing UI:
const usage = await byok.usage.get({ refId: 'customer_123' });
console.log(usage.totals.costUsd, usage.byModel, usage.daily);
// Filter by date range and provider; pass Date or ISO string.
const last7d = await byok.usage.get({
refId: 'customer_123',
provider: 'openai',
since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
});
// 24-hour rollup used by the dashboard overview.
const stats = await byok.usage.stats();The cost is calculated server-side using BYO's built-in pricing table. If
you call a model BYO doesn't have a price for (a fine-tune, a fresh model
release, an OpenAI-compatible local server), it'll show up in
usage.unknownModels so you know exactly what to add — and the cost row
will be null until you patch it:
console.log(usage.unknownModels);
// [{ provider: 'openai', model: 'ft:gpt-4o-...:abc',
// requests: 142, totalTokens: 184230 }]Custom model prices
Patch in custom prices per project, useful for fine-tunes, niche
OpenAI-compatible endpoints, or anything BYO ships without a default for.
The shape mirrors the BYO_MODEL_PRICING env var; numbers are USD per
1,000,000 tokens. Changes take effect within ~60s on every API instance.
await byok.pricing.update(projectId, {
openai: { 'my-finetune': { input: 0.5, output: 2.0 } },
});
const { overrides } = await byok.pricing.get(projectId);
await byok.pricing.clear(projectId); // back to BYO's defaultsStreaming note: to capture token counts on streamed OpenAI Chat Completions, include
stream_options: { include_usage: true }in the request — otherwise providers don't return ausageblock and the log row will havetokensandcostUsdset tonull.
Logs
Inspect individual proxied requests, including the captured tokens and cost:
const { data, total } = await byok.logs.list({
refId: 'customer_123',
success: false,
limit: 50,
});
const log = await byok.logs.get(data[0].id);
console.log(log.model, log.totalTokens, log.costUsd);Error Handling
The SDK throws typed errors so you can branch on the failure mode without parsing status codes by hand:
| Class | When it's thrown |
| ---------------------- | -------------------------------------------------- |
| AuthenticationError | 401 — invalid or missing API key |
| AuthorizationError | 403 — key isn't allowed to perform this action |
| ValidationError | 400 / 422 — bad payload |
| NotFoundError | 404 — project / log / endpoint missing |
| RateLimitError | 429 — includes retryAfterMs from Retry-After |
| ProviderError | Upstream provider (OpenAI, Anthropic, …) rejected |
| ServerError | 5xx from the BYO API |
| NetworkError | Connection failure (fetch threw) |
| TimeoutError | Hit the SDK request timeout |
| BYOKError | Base class — catch this for "anything from BYO" |
import {
BYOK,
BYOKError,
AuthenticationError,
RateLimitError,
ProviderError,
} from 'byo-sdk';
try {
const response = await openai.responses.create({ ... });
} catch (err) {
if (err instanceof AuthenticationError) {
console.error('Invalid API key');
} else if (err instanceof RateLimitError) {
console.error(`Rate limited, retry after ${err.retryAfterMs ?? 'unknown'}ms`);
} else if (err instanceof ProviderError) {
console.error(`Upstream ${err.provider} rejected the request: ${err.message}`);
} else if (err instanceof BYOKError) {
console.error(`Error ${err.statusCode}: ${err.message}`);
}
}Webhooks
Verify webhook deliveries from BYO with a single call. The signature is a constant-time HMAC-SHA256 over the raw request body using the endpoint's signing secret:
import { constructWebhookEvent } from 'byo-sdk';
app.post('/webhooks/byo', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = constructWebhookEvent(
req.body, // raw Buffer, do NOT parse first
req.header('X-BYO-Signature'),
process.env.BYO_WEBHOOK_SECRET!,
);
handleEvent(event);
res.status(200).end();
} catch {
res.status(400).end();
}
});Need just the boolean? Use verifyWebhookSignature(payload, header, secret).
Project Origin Allowlist
When you ship a publishable key (byo_pk_*) into a frontend, lock it down to
the origins that should be allowed to call /keys/connect and /connect/form:
await byok.origins.update('proj_123', [
'https://app.example.com',
'https://staging.example.com',
]);
// Empty list = unrestricted (back to the previous behaviour)
await byok.origins.clear('proj_123');Audit Log
Every security-relevant mutation (project changes, key revokes, pricing updates, origin allowlist edits, webhook config changes) is recorded in an append-only audit log:
const { data } = await byok.audit.list({
projectId: 'proj_123',
action: 'provider_key.revoked',
since: new Date(Date.now() - 24 * 60 * 60 * 1000),
});