Fluent, resilient, type-safe AI SDK for OpenAI-compatible chat models.
Lowdeep helps you move from free-form model output to validated TypeScript objects. It uses Zod for runtime validation and retries automatically when responses do not match your schema.
- Why Lowdeep
- Installation
- Requirements
- Quick Start
- Detailed Examples
- How the Builder Works
- API Reference
- Self-Healing JSON Flow
- Provider Behavior
- Error Handling
- Development
- Security Notes
- License
- Fluent builder API:
lowdeep().key(...).model(...).chat(...) - Type-gated usage:
chat()is only available after required setup - Zod output validation with inferred TypeScript return types
- Optional Zod input validation before making provider requests
- Retry-on-validation-error loop with feedback sent back to the model
- Built-in conversation memory, plus custom history injection
bun add lowdeep zod
# or
npm install lowdeep zod- Node.js or Bun
- TypeScript
>=5 - API key for one of the supported providers
import lowdeep from "lowdeep";
const ai = lowdeep()
.key(process.env.OPENAI_API_KEY!)
.model("gpt-4o-mini")
.system("Be practical and concise.")
.temperature(0.4);
const answer = await ai.chat("Explain what an API is in one paragraph.");
console.log(answer);Use this when you only need plain text and do not need schema validation.
import lowdeep from "lowdeep";
const ai = lowdeep()
.key(process.env.GROQ_API_KEY!)
.model("llama-3.3-70b-versatile")
.retry(2);
const tips = await ai.chat("Give me 5 ways to learn TypeScript faster.");
console.log(tips);Use output schema when you need deterministic object shapes.
import { z } from "zod";
import lowdeep from "lowdeep";
const PlanSchema = z.object({
topic: z.string(),
difficulty: z.enum(["beginner", "intermediate", "advanced"]),
steps: z.array(
z.object({
title: z.string(),
estimateMinutes: z.number().int().min(1),
}),
),
});
const ai = lowdeep()
.key(process.env.OPENAI_API_KEY!)
.model("gpt-4o-mini")
.schema(PlanSchema)
.retry(3);
const plan = await ai.chat("Create a React hooks study plan for beginners.");
// typed values from z.infer<typeof PlanSchema>
console.log(plan.topic);
console.log(plan.steps[0]?.title);Use this when your input is also structured and must be validated before request.
import { z } from "zod";
import lowdeep from "lowdeep";
const InputSchema = z.object({
productName: z.string().min(2),
audience: z.enum(["developer", "manager", "founder"]),
tone: z.enum(["serious", "friendly"]),
});
const OutputSchema = z.object({
headline: z.string(),
bullets: z.array(z.string()).length(3),
cta: z.string(),
});
const ai = lowdeep()
.key(process.env.OPENAI_API_KEY!)
.model("gpt-4o-mini")
.schema(OutputSchema, InputSchema)
.system("Return concise marketing copy.");
const ad = await ai.chat({
productName: "Lowdeep",
audience: "developer",
tone: "friendly",
});
console.log(ad.headline);
console.log(ad.bullets);
console.log(ad.cta);If input does not match InputSchema, chat(...) throws immediately and no request is sent.
Lowdeep keeps conversation history in memory for the builder instance.
import lowdeep from "lowdeep";
const ai = lowdeep()
.key(process.env.OPENAI_API_KEY!)
.model("gpt-4o-mini");
await ai.chat("Remember this code: PX-19.");
const reply = await ai.chat("What code did I ask you to remember?");
console.log(reply);Use .use(...) to preload context from your application state.
import lowdeep from "lowdeep";
import type { ChatCompletionMessageParam } from "openai/resources";
const history: ChatCompletionMessageParam[] = [
{ role: "system", content: "You are a strict project assistant." },
{ role: "user", content: "Project codename is Atlas." },
];
const ai = lowdeep()
.use(history)
.key(process.env.OPENAI_API_KEY!)
.model("gpt-4o-mini");
console.log(await ai.chat("What is the project codename?"));Use .chatStream(...) for real-time text streaming in chat applications or CLI tools.
import lowdeep from "lowdeep";
const ai = lowdeep()
.key(process.env.OPENAI_API_KEY!)
.model("gpt-4o-mini");
const stream = await ai.chatStream("Write a haiku about clean code.");
for await (const token of stream) {
process.stdout.write(token);
}Connect to Ollama, vLLM, LocalAI, or corporate proxies with .baseURL(...).
import lowdeep from "lowdeep";
const ai = lowdeep()
.baseURL("http://localhost:11434/v1")
.key("ollama")
.model("llama3.2");
const reply = await ai.chat("Hello from local Ollama!");
console.log(reply);Track attempts and catch self-healing retries in production logging.
import lowdeep from "lowdeep";
import { z } from "zod";
const ai = lowdeep()
.key(process.env.GROQ_API_KEY!)
.model("llama-3.3-70b-versatile")
.schema(z.object({ status: z.literal("success") }))
.onAttempt((attempt, max) => {
console.log(`[Attempt ${attempt}/${max}]`);
})
.onRetry((error, attempt, max) => {
console.warn(`[Retry ${attempt}/${max}] Schema failed, requesting healing:`, error);
});
const data = await ai.chat("Return status success");Cancel long-running requests or retries when a user navigates away:
import lowdeep from "lowdeep";
const ai = lowdeep()
.key(process.env.OPENAI_API_KEY!)
.model("gpt-4o");
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 2000);
try {
const reply = await ai.chat("Generate an extensive essay", {
signal: controller.signal,
timeoutMs: 10000,
});
} catch (err: any) {
if (err.name === "AbortError") {
console.log("Request successfully aborted!");
}
}Lowdeep builders are 100% immutable. Create base configurations and derive specialized agents without side-effects or state pollution:
import lowdeep from "lowdeep";
const base = lowdeep()
.key(process.env.OPENAI_API_KEY!)
.model("gpt-4o-mini");
// Deriving two isolated agents
const copywriter = base.system("You are an expert copywriter.");
const reviewer = base.system("You are a strict code reviewer.");
// copywriter and reviewer maintain completely separate histories and promptsTypical order:
lowdeep().key(...).model(...)- Optional config (
.system(),.temperature(),.retry(),.schema(),.use()) .chat(...)
Type behavior:
- You cannot call
chat()untilkeyandmodelare configured. - Without output schema, return type is text.
- With output schema, return type is inferred from Zod.
Creates a new builder instance.
Sets API key and infers provider from key prefix:
gsk_->groqsk-or-->openroutersk_orsk-proj-->openaitogether_->together- any other prefix ->
deepinfra(orollamaif localhost)
Sets model id passed to the provider.
Sets a custom OpenAI-compatible endpoint URL (e.g. for Ollama http://localhost:11434/v1, vLLM, or corporate proxies).
Sets system instruction. Default: "Be a helpful assistant".
Sets temperature from 0 to 2.
Throws LowdeepConfigurationError for values outside this range.
Default: 0.7.
Sets max retry attempts for the self-healing loop.
Default: 3.
Enables or disables console logging during attempts. Disabled by default for clean production logs.
Controls how structured output is requested from the model:
"auto"(default): Uses native OpenAI-compatibleresponse_format: { type: "json_schema" }and automatically falls back to prompt injection if the model does not support it."strict": Enforces strict nativejson_schema."prompt": Disables nativeresponse_formatand uses pure system prompt guidance with Lowdeep's balanced JSON parser.
Hook invoked at each chat attempt.
Hook invoked when a validation error occurs before initiating a self-healing retry.
outputSchema: validates model response and returns typed objectinputSchema: validateschat(data)payload before provider request
Creates an isolated duplicate of the current builder instance with cloned conversation history.
Replaces current internal history with your own message array.
Returns a shallow copy of the current message history.
Resets current message history to an empty array.
data: prompt string or structured payload conforming toinputSchema.options: optionalCallOptionscontaining:signal?: AbortSignal(to cancel active request and retries)timeoutMs?: number(request timeout in milliseconds)headers?: Record<string, string>(custom HTTP headers)
- Returns validated typed data when
outputSchemais set, or raw string otherwise. - Throws
LowdeepMaxRetriesErrorif retries are exhausted without a valid schema match.
Streams model text output token by token as an AsyncGenerator<string, void, unknown>. Accepts optional CallOptions (signal, timeoutMs, headers).
A runtime provider("groq" | "openai" | "deepinfra" | ...) method exists for compatibility, but key-based provider inference or explicit baseURL is the recommended approach.
When output schema is configured, Lowdeep:
- Injects JSON schema guidance in the system message.
- Requests strict JSON output.
- Cleans model output (including fenced JSON or reasoning
<think>tags). - Parses and validates with Zod.
- On failure, appends validation errors and triggers
onRetryhook. - Automatically requests correction from the model in an atomic retry loop.
If all retries fail, Lowdeep throws a LowdeepMaxRetriesError containing the attempt count, the last raw response, and the underlying validation errors.
Supported providers (auto-inferred or configured via baseURL):
- OpenAI
- Groq
- DeepInfra
- OpenRouter
- Together AI
- Ollama / vLLM / LocalAI (via
.baseURL("http://localhost:11434/v1"))
All requests are sent through OpenAI-compatible chat completions with HTTP connection pooling.
Lowdeep exports custom error classes:
LowdeepError: Base error class.LowdeepConfigurationError: Thrown for invalid configurations (e.g. temperature out of range, missing key/model).LowdeepValidationError: Thrown when input data violatesinputSchema.LowdeepMaxRetriesError: Thrown when all self-healing attempts fail.
Recommended pattern:
import lowdeep, {
LowdeepConfigurationError,
LowdeepMaxRetriesError,
LowdeepValidationError,
} from "lowdeep";
try {
const result = await ai.chat("Return JSON with title and score");
console.log(result);
} catch (error) {
if (error instanceof LowdeepValidationError) {
console.error("Input validation failed:", error.issues);
} else if (error instanceof LowdeepMaxRetriesError) {
console.error(`Failed after ${error.attempts} attempts. Last response:`, error.lastResponseContent);
} else {
console.error("Lowdeep request failed:", error);
}
}Build the package:
bun run buildRun test suite:
bun testBuild output is generated in dist/.
- Do not hardcode API keys in committed files.
- Prefer
process.env.*for secrets. - Rotate keys immediately if exposed.
MIT