Typed AI Clients: Building Safe TypeScript Interfaces for On‑Device LLMs
Build robust TypeScript SDKs for on-device LLMs: typed schemas, runtime validation, safety filters, and offline fallbacks for Pi HATs and local agents.
Hook: You're shipping on-device AI — but are your TypeScript SDK types keeping users safe?
On-device LLMs and desktop AI agents (think Raspberry Pi AI HATs, llama.cpp builds, or local Claude/Cowork-like services) promise low latency, privacy, and offline-first experiences. But they also surface a different set of risks: unpredictable outputs, resource constraints, and the need for deterministic client behavior across flaky hardware. If your TypeScript SDK exposes an untyped or weakly typed surface, runtime surprises become bugs, safety audits become painful, and migration from JavaScript stalls.
Executive summary (most important first)
In 2026, building a production-ready, typed client for on-device AI is three disciplines in one:
- Type-safe contracts — design TypeScript types and runtime schemas for requests and structured responses.
- Robust runtime validation & safety — enforce schemas, filter hazardous content, and include lower-risk fallbacks.
- Offline-first resilience — fallback to quantized models, cached responses, or cloud proxies when local resources fail.
This article walks through patterns, concrete code, packaging tips for DefinitelyTyped/typings, and 2026-specific trends you must account for (Pi AI HAT+ hardware, desktop agents like Cowork, WASM & WebGPU deployments, and newer privacy regulations).
2026 context: why typed on-device clients matter
The last 18 months saw two big shifts that matter to SDK designers:
- Consumer and edge AI hardware matured. Devices like the Raspberry Pi 5 + AI HAT 2 and optimized quantized model runtimes (llama.cpp, GGML variants, tinyLLM) make capable on-device LLMs practical.
- Desktop AI agents with filesystem access and autonomy are mainstream, driven by tools like Anthropic's Cowork research previews and other local-first apps. Those increase the attack surface for prompt-based tasking.
That means your SDK will run in constrained environments and sometimes offline — and it must behave predictably. TypeScript static types are only one part of the solution: you also need runtime validation and safety gates.
Design goals for a typed on-device LLM client
- Clear API surface: small, composable operations with typed inputs and outputs.
- Runtime-synced schemas: TypeScript types coupled to a runtime validator (zod/io-ts/JSON Schema) so types are enforced at runtime.
- Safety-first defaults: block or sanitize high-risk outputs, and expose safe opt-in configuration for permissive scenarios.
- Offline-first fallback: graceful degradation to a lighter model, cached answer, or queue for later cloud processing.
- Minimal bundle size: keep runtime validators and quantized runtime footprints small for edge devices.
Core patterns and technologies
1. Types + runtime schemas: Zod-driven contracts
TypeScript's types vanish at runtime. To avoid a divergence between compile-time expectation and runtime reality, pair types with a runtime schema. Zod is a compact choice in 2026; it compiles well with bundlers like tsup and supports inference with z.infer.
import { z } from 'zod'
// Input schema for a structured generation request
export const GenRequestSchema = z.object({
prompt: z.string().min(1),
maxTokens: z.number().int().min(1).max(2048).optional(),
temperature: z.number().min(0).max(2).optional(),
metadata: z.record(z.string()).optional(),
})
export type GenRequest = z.infer<typeof GenRequestSchema>
// Structured output schema (prefer structured responses to free text!)
export const GenResponseSchema = z.object({
id: z.string(),
text: z.string(),
language: z.string().optional(),
safetyLabels: z.array(z.string()).optional(),
})
export type GenResponse = z.infer<typeof GenResponseSchema>
Why structured responses? They let you verify and act on fields (detected intent, entities, commands) instead of parsing free text.
2. Runtime validation & type guards
Wrap internal model outputs with validators immediately after receiving them. Fail early and return typed errors, never raw unverified objects to consumers.
async function validateResponse(raw: unknown): Promise<GenResponse> {
const result = GenResponseSchema.safeParse(raw)
if (!result.success) {
throw new TypeError('Model produced invalid response: ' + JSON.stringify(result.error.format()))
}
return result.data
}
3. Safety checks & filtering pipeline
A layered safety pipeline reduces false negatives and offers auditable steps:
- Pre-prompt sanitization — remove or replace sensitive tokens in prompt metadata.
- Post-generation content filtering — use regexes and a typed policy to drop or redact PII, unsafe instructions, or filesystem commands.
- Policy labels — map risky content to enumerated labels and block or flag based on config.
export type SafetyLevel = 'allow' | 'warn' | 'block'
export const SafetyLabelSchema = z.enum(['pii', 'malware-instruction', 'injection', 'abuse'])
function applySafetyPipeline(resp: GenResponse, config: { level: SafetyLevel }) {
const labels = resp.safetyLabels ?? []
if (labels.includes('malware-instruction') || labels.includes('injection')) {
if (config.level === 'block') throw new Error('Blocked by safety policy')
if (config.level === 'warn') console.warn('Safety warning:', labels)
}
// Example redaction
resp.text = resp.text.replace(/\b(\d{4}-\d{4}-\d{4}-\d{4})\b/g, '[REDACTED-CARD]')
return resp
}
4. Offline-first fallbacks
On-device models fail for many reasons: thermal throttling, memory exhaustion, or missing quantized weights. A robust client includes a prioritized fallback plan. Example fallback chain:
- Primary: local quantized model via WASM or native runtime
- Fallback A: smaller distilled local model
- Fallback B: cached answer from local index
- Fallback C: deferred cloud proxy (when privacy policy allows)
type FallbackStrategy = 'distilled' | 'cache' | 'cloud'
async function runWithFallback(req: GenRequest, strategies: FallbackStrategy[] = ['distilled', 'cache', 'cloud']): Promise<GenResponse> {
// try primary local runtime
try {
const raw = await runLocalModel(req)
const resp = await validateResponse(raw)
return applySafetyPipeline(resp, { level: 'warn' })
} catch (err) {
console.warn('Primary model failed:', err)
}
for (const s of strategies) {
try {
if (s === 'distilled') {
const raw = await runDistilledModel(req)
const resp = await validateResponse(raw)
return applySafetyPipeline(resp, { level: 'warn' })
}
if (s === 'cache') {
const cached = getCachedResponse(req)
if (cached) return cached
}
if (s === 'cloud') {
const raw = await runCloudProxy(req)
const resp = await validateResponse(raw)
return applySafetyPipeline(resp, { level: 'warn' })
}
} catch (err) {
console.warn('Fallback', s, 'failed:', err)
}
}
throw new Error('All fallbacks failed')
}
5. Typed streaming responses
Edge LLM runtimes increasingly support streaming tokens. Provide a typed streaming API so consumers can react to partial structured outputs and cancellations.
import { EventEmitter } from 'events'
export interface StreamChunk { token: string; isFinal?: boolean }
export class LlmStream extends EventEmitter {
// 'chunk' gives typed data; 'error' and 'end' events too
}
function streamGeneration(req: GenRequest): LlmStream {
const stream = new LlmStream()
// wire native runtime token callbacks to stream.emit('chunk', { token })
return stream
}
Use a typed emitter interface (or Observables/AsyncIterator) so TypeScript understands event payloads.
Concrete example: a tiny typed SDK for a Pi AI HAT
Below is a minimal SDK layout focused on types, validation, safety, and fallbacks. This is a condensed version; a production package should include logging, telemetry toggles, and strong testing.
// src/index.ts
export type ClientConfig = {
deviceId: string
safetyLevel?: SafetyLevel
preferLocal?: boolean
}
export class OnDeviceAIClient {
constructor(private config: ClientConfig) {}
async generate(req: GenRequest): Promise<GenResponse> {
// validate input
const parsed = GenRequestSchema.parse(req)
// offline-first call
const resp = await runWithFallback(parsed, this.config.preferLocal ? ['distilled', 'cache'] : ['cache', 'cloud'])
return resp
}
}
Key features shown:
- Input schema validation before any model is invoked.
- Fallback chain controlled by config.
- Typed output guaranteed via validateResponse.
Packaging, typings, and contributing to DefinitelyTyped
Ship strong types for your consumers and make integrations easy:
- Include declaration files: build with tsup/tsc to generate index.d.ts. Example tsconfig should set declaration: true and composite: false for libraries. See guides like From ChatGPT prompt to TypeScript micro app for practical TypeScript packaging patterns.
- Use the exports field and typesVersions: in package.json, provide explicit ESM/CJS entry points and types mapping for different TS versions.
- Keep runtime validator optional: export a lightweight runtime and a fuller dev bundle. Edge SDKs can tree-shake validators if the consumer prefers trust over safety (document risks clearly).
- DefinitelyTyped: if you ship JS-only runtimes or native modules, include high-quality .d.ts or open a PR to DefinitelyTyped with examples and tests. In 2026, consumers expect typed runtimes for edge AI libraries.
Example package.json snippets
{
'name': '@yourorg/ondevice-ai',
'version': '1.0.0',
'main': 'dist/index.cjs.js',
'module': 'dist/index.esm.js',
'types': 'dist/index.d.ts',
'exports': {
'.': {
'import': './dist/index.esm.js',
'require': './dist/index.cjs.js'
}
}
}
Testing & CI: type-aware end-to-end tests
Unit tests should not only check runtime behavior but verify schema compliance and safety checks. Add contract tests that ensure mocked model outputs always validate with your schemas. Use small binary quantized models in CI for integration tests when possible, or canned fixtures representing runtime variants.
2026 safety & regulatory considerations
Regulatory attention to AI transparency and on-device autonomy has increased. Keep these in mind:
- Privacy-first defaults: default to local compute and only enable cloud fallbacks when explicitly allowed by policy/consent. See privacy-first personalization playbooks for design parallels.
- Auditable logs: provide optional, privacy-preserving audit logs for decisions and blocked outputs (hashed or reversibly encrypted under a key controlled by the user).
- Explainability: return structured metadata (model version, temperature, safety labels) in responses so applications can explain decisions.
Advanced strategies: personalization, model selection, and typed adapters
When you add personalization or multi-model fleets, use typed adapters and a model registry to keep interfaces stable.
- Model descriptors: typed object describing model capabilities: { id, sizeMB, quantized, supportsStreaming }. Use these to pick fallback chains.
- Typed adapters: for each runtime (WASM, native, cloud), implement a typed adapter exposing the same GenRequest -> GenResponse contract.
- Feature flags: let downstream apps enable features (file system access, exec commands) behind typed, audited opt-ins.
Common pitfalls and how to avoid them
- Weak types at the boundary: never return raw any. Always validate before returning.
- Bundling heavy validators: keep a small core. Offer a dev bundle with full validation and a slim edge bundle with minimal runtime checks plus a documented escape hatch.
- Opaque errors: surface typed error shapes (code, message, recoverySuggestion) so apps can automate recovery.
- Silent policy changes: version your safety policies and include policyVersion on responses for traceability.
Actionable checklist for your next release
- Design request/response schemas with zod or JSON Schema and infer TS types.
- Add validation at every model boundary and convert errors to typed exceptions.
- Implement a 3-step offline fallback chain and test it on low-RAM devices.
- Expose streaming with typed events or AsyncIterator.
- Ship declaration files and export ESM/CJS entries with types mapped.
- Document safety defaults and include a policy version in responses.
Future-proofing & predictions for 2026+
Expect these trends to affect typed SDK design over the next 12–24 months:
- Widespread WASM + WebGPU runtimes will make in-browser on-device LLMs more common; keep an eye on bundle size and tree-shaking of validators.
- Model heterogeneity will increase: plan for typed adapters and model capability probing at runtime.
- Regulation will push stronger default safety and explicit user consent for cloud fallbacks; make consent and auditability first-class in your API.
2026 take: typed clients are more than developer ergonomics — they're a compliance, safety, and UX toolchain.
Final takeaways
- Couple static types with runtime schemas so compile-time guarantees match what actually runs on-device.
- Design safety as explicit, typed policies that your SDK enforces and reports about.
- Plan offline-first fallbacks and make them configurable and auditable.
- Ship types and small runtimes and contribute declaration files to DefinitelyTyped when appropriate. For packaging patterns consider modular installer and distribution approaches like modular installer bundles.
Call to action
If you're building or migrating a client for on-device AI, start by defining a minimal GenRequest/GenResponse schema and add a single runtime validator. Try running your SDK on a Raspberry Pi 5 + AI HAT or a small VM configured like a user's edge device. Need help turning that prototype into a typed, auditable SDK? Join our community on GitHub, open an issue with your schema, or request a review — we can walk through packaging, type exports, and safety policy design together.
Related Reading
- Designing Privacy-First Personalization with On-Device Models — 2026 Playbook
- From ChatGPT prompt to TypeScript micro app: automating boilerplate generation
- Making Diagrams Resilient in 2026: Offline-First Tooling, Type-Safe Exports, and Observability
- Zero Trust for Generative Agents: Designing Permissions and Data Flows for Desktop AIs
- Top 7 Battery Backups for Home Offices: Capacity, Noise, and Price (Jackery, EcoFlow, More)
- Affordable Tech for Skin Progress Photos: Gear You Need to Make Before & After Shots Look Professional
- Typewriter Critic: How to Write Sharply Opinionated Essays About Fandom Using Typewritten Form
- Local Sellers: Where to Find Pre-Loved Wearable Microwavable Warmers Near You
- Trusts and Long-Term Service Contracts: Who Reviews the Fine Print?
Related Topics
typescript
Contributor
Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.
Up Next
More stories handpicked for you