OpenAI-compatible
The OpenAI /chat/completions format is the industry default — OpenAI, Mistral, OpenRouter, Groq,
Together, Z.ai, LM Studio and Ollama (/v1) all speak it. This one zero-dependency adapter
covers the whole family; vendors differ only by data (base URL, branding), which you pass as config
or pick from presets.
npm install @aparte/provider-openai-compat @aparte/coreimport { aparteGlobalConfig, AparteDirectTransport } from '@aparte/core';import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';
// A known vendor, via a preset:aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider(presets.MISTRAL));
// …or any compat endpoint, no preset needed — just an id + base URL:aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider({ id: 'groq', baseURL: 'https://api.groq.com/openai/v1',}));
aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));Built-in presets: OPENAI, MISTRAL, ZAI, OPENROUTER, LMSTUDIO, OLLAMA.
Models are your data
Section titled “Models are your data”Pass a static models list, or rely on the generic GET {baseURL}/models fetcher (part of the
compat standard). Cloud endpoints need a key to list; local servers list keyless.
createOpenAICompatProvider({ id: 'x', baseURL: 'https://x.example/v1', models: [{ id: 'llama-3.3-70b', name: 'Llama 3.3 70B' }],});Local models (LM Studio, Ollama)
Section titled “Local models (LM Studio, Ollama)”Local servers are served through their OpenAI-compat /v1 endpoint. The isLocal presets relax
the key requirement and fetch models keyless:
aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider(presets.OLLAMA)); // http://localhost:11434/v1aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));The full recipe — CORS, picking a model from the server’s list, llama.cpp without a preset, the context window — is Chat UI for a local LLM.
As a pure format adapter (bring your own fetch)
Section titled “As a pure format adapter (bring your own fetch)”The provider separates wire format from transport, so you can take the first and keep the
second. createOpenAICompatProvider returns an OpenAICompatProvider — the provider type plus
the format-adapter surface, with buildRequest / parseStream / authHeaders / parseText
guaranteed present (the base AparteAIProvider declares them optional, since a provider may do its
own I/O):
import { readableToAsyncIterable } from '@aparte/core';import type { AparteChatRequest } from '@aparte/core';import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';
const provider = createOpenAICompatProvider(presets.MISTRAL);
const baseURL = 'https://api.mistral.ai/v1';const apiKey = process.env['MISTRAL_API_KEY'] ?? '';const request: AparteChatRequest = { messages: [{ role: 'user', content: 'hi' }], modelId: 'mistral-small-latest', stream: true };
// You own the call: your URL, your headers, your AbortSignal, your retries.// `buildRequest` takes the request alone; the key goes through `authHeaders`.const { path, body, headers } = provider.buildRequest(request);const res = await fetch(`${baseURL}${path}`, { method: 'POST', headers: { ...headers, ...provider.authHeaders(apiKey) }, body: JSON.stringify(body),});
// `parseStream` returns a ReadableStream, which is NOT async-iterable in Chromium// (or under `lib: DOM`). `readableToAsyncIterable` is core's adapter for exactly this.const controller = new AbortController(); // yours to abort — the helper honours itfor await (const event of readableToAsyncIterable(provider.parseStream(res.body!), controller.signal)) { // text · thinking · tool_use · done{usage} — typed, vendor quirks already handled void event;}No transport, no AparteClient, no DOM: useful when the loop lives somewhere else entirely (a
separate process, a server route, an Electron main process).
For vendors outside this family (Anthropic, Gemini…), use the AI SDK bridge.