Skip to content

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 transportwhere the request goes and how the key is handled: AparteDirectTransport (browser → provider) or AparteBackendTransport (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-side
new 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 loop

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 set isLocal, so keyResolver can return undefined for them. Run one and you need no key at all.
  • Want the key off the client entirely? Use AparteBackendTransport instead — the key stays on your server and never reaches the browser.
  • keyResolver may return a Record<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.
You want to reach…Package
OpenAI, Mistral, OpenRouter, Groq, Together, Z.ai, LM Studio, Ollamaopenai-compat
Anthropic, Google, Amazon Bedrock, or any other @ai-sdk/* vendorai-sdk
A model running 100% in the browser — no server, no keytransformers

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 what openai-compat does.
  • Own-I/O (chat()) — the provider makes its own request (an SDK, a local runtime); AparteDirectTransport delegates to it and forwards the abort signal. This is what ai-sdk and transformers do.

Both shapes share the identity part: getMetadata() returns an AparteAIProviderMetadataid, 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']>.