Unified, zero-dependency TypeScript facade over LLM chat-completion providers. One typed Chat facade; switch vendor by changing the provider id + model. Stateless, bring-your-own fetch, no vendor SDKs — the in-process, zero-egress complement to an LLM gateway.
npm install @thinwrap/llmNode ≥ 18 (native fetch).
import { Chat } from '@thinwrap/llm';
const chat = new Chat('openai', { apiKey: process.env.OPENAI_API_KEY! });
const res = await chat.complete({
model: 'gpt-5.6',
messages: [{ role: 'user', content: 'Say hi in one word.' }],
});
console.log(res.message.content, res.usage);Switching vendor is the provider id + model:
const chat = new Chat('anthropic', { apiKey: process.env.ANTHROPIC_API_KEY! });
// same .complete(input) shape, same ChatResultStreaming:
for await (const delta of new Chat('groq', { apiKey }).stream(input)) {
if (delta.contentDelta) process.stdout.write(delta.contentDelta);
}Facade → connector dispatch → shared OpenAICompatConnector (or a native adapter). The normalized facade holds only the ≥90%-supported core; provider-specific features ride through _passthrough (request) / raw (result). See the contributor docs in .ai/.
Per-provider docs (auth, config, quirks, rate-limit links, error mapping) live in
src/providers/ — one README per provider.
The wrapper is stateless and holds no timeout of its own — it awaits whatever your fetch does. To bound a hung provider, bring your own timeout via the injected fetch:
const chat = new Chat('openai', {
apiKey: process.env.OPENAI_API_KEY,
fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(30_000) }),
});An aborted request surfaces as a ConnectorError (providerCode: 'invalid_request'), same as every other failure.
MIT © Dmitry Polyanovsky