Providers — OpenAI-Compatible, Vercel AI SDK, In-Browser, Scripted Demo
@aparte/core is model-agnostic: it never talks to a vendor directly. Two composable pieces do that:
- A provider — the wire-format adapter for a model family (how a request is shaped, how the
response stream is parsed). Providers ship as opt-in
@aparte/provider-*packages, so core stays zero-dependency. - A transport — where the request goes and how the key is handled:
AparteDirectTransport(browser → provider) orAparteBackendTransport(browser → your server). See Getting started.
A fourth package is a provider that calls no model at all: @aparte/provider-scenario
replays turns you wrote — for a demo that streams without a key, and for your own tests, which get a
deterministic model.
Register a provider, set a transport, construct an AparteClient, and streaming just works:
import { aparteGlobalConfig, AparteClient, AparteDirectTransport } from '@aparte/core';import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';
aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider(presets.OPENROUTER));aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true })); // browser → provider, key stays client-sidenew AparteClient({ // BYOK: hand the browser-held key to each request (see "Supplying the key" below). keyResolver: () => localStorage.getItem('openrouter.key') ?? undefined,}).start(); // .start() drives the streaming loopSupplying the API key (BYOK)
Section titled “Supplying the API key (BYOK)”A cloud provider needs a key — without one it returns an empty model list and can’t stream.
With AparteDirectTransport({ byok: true }) the key lives in the browser (it’s never sent to a
server), and you hand it to each request via keyResolver on AparteClient:
new AparteClient({ // Called per request with the providerId; return the key (or undefined for none). keyResolver: (providerId) => localStorage.getItem(`${providerId}.key`) ?? undefined,}).start();The canonical BYOK flow: a small key <input> in your UI writes the value to localStorage,
and keyResolver reads it back — so no key is ever hard-coded or committed:
keyInput.addEventListener('change', () => { localStorage.setItem('openrouter.key', keyInput.value.trim());});- Local providers (
presets.LMSTUDIO,presets.OLLAMA) are keyless — they setisLocal, sokeyResolvercan returnundefinedfor them. Run one and you need no key at all. - Want the key off the client entirely? Use
AparteBackendTransportinstead — the key stays on your server and never reaches the browser. keyResolvermay return aRecord<string, string>(for providers needing several auth headers) and may be async (fetch from your own vault).aparteGlobalConfig.setKeyProvider(...)is an alternative channel if you’d rather register the key globally instead of per-client.
Which one?
Section titled “Which one?”| You want to reach… | Package |
|---|---|
| OpenAI, Mistral, OpenRouter, Groq, Together, Z.ai, LM Studio, Ollama | openai-compat |
Anthropic, Google, Amazon Bedrock, or any other @ai-sdk/* vendor | ai-sdk |
| A model running 100% in the browser — no server, no key | transformers |
Writing your own
Section titled “Writing your own”A provider is any object implementing the AparteAIProvider interface. There are two shapes, and
both surfaces are optional — implement only the half you need:
- Format adapter (
buildRequest/authHeaders/parseStream/parseText+defaultEndpoint) — the provider only shapes the payload and parses the stream; a transport owns auth and the network. This is whatopenai-compatdoes. - Own-I/O (
chat()) — the provider makes its own request (an SDK, a local runtime);AparteDirectTransportdelegates to it and forwards the abort signal. This is whatai-sdkandtransformersdo.
Both shapes share the identity part: getMetadata() returns an AparteAIProviderMetadata
— id, name, an icon (SVG) and a color — which is what the model picker and the
provider select display. The type is exported, so a provider written outside this
repository can annotate its own getMetadata() with it rather than spell it
ReturnType<AparteAIProvider['getMetadata']>.