Skip to content

Connect OpenAI and every OpenAI-compatible endpoint

One adapter for every endpoint that speaks OpenAI's chat completions — OpenAI, Mistral, OpenRouter, Z.ai, Ollama, LM Studio, and any other compatible URL such as Groq or vLLM.

On this page
bash
npm install @aparte/core @aparte/provider-openai-compat
ts
import { aparteGlobalConfig } from '@aparte/core';
import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';

aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider(presets.MISTRAL));

aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider({
  id: 'groq',
  name: 'Groq',
  baseURL: 'https://api.groq.com/openai/v1',
}));

createOpenAICompatProvider takes an OpenAICompatProviderOptions. A preset is one filled in — a vendor’s base URL and branding — and any other endpoint takes an id and its baseURL. Spread a preset to change a field: { ...presets.OPENAI, baseURL: 'https://my-proxy.example/v1' }. The key goes through the key resolver. Nothing is selected until you name a model — aparteGlobalConfig.setModelConfig({ defaultProvider: 'mistral', defaultModel: 'mistral-small-latest' }) — or the person picks one in the model selector.

PresetBase URLKey
presets.OPENAI — OpenAIhttps://api.openai.com/v1yours
presets.MISTRAL — Mistral AIhttps://api.mistral.ai/v1yours
presets.ZAI — Z.ai (Zhipu)https://open.bigmodel.cn/api/paas/v4yours
presets.OPENROUTER — OpenRouterhttps://openrouter.ai/api/v1yours
presets.LMSTUDIO — LM Studiohttp://localhost:1234/v1none, a server on your machine
presets.OLLAMA — Ollamahttp://localhost:11434/v1none, a server on your machine

presets.OPENROUTER sends OpenRouter’s attribution headers, HTTP-Referer with the page’s origin and X-Title: aparté; name your app with { ...presets.OPENROUTER, extraHeaders: { 'HTTP-Referer': location.origin, 'X-Title': 'My app' } }.

The provider fetches GET {baseURL}/models, and the model selector lists each model by its name, or its id when the server sends none; a model’s context_length becomes the contextWindow the context gauge reads. Each fetched model is marked as streaming and calling tools. A cloud provider fetches nothing without a key; a local one — isLocal: true, which the Ollama and LM Studio presets set — lists its models with none. A models option gives the provider a list of its own, which the client reads for a model the fetched list does not have: a fetched entry with the same id wins, and the selector shows only the fetched list.

ts
import { aparteGlobalConfig } from '@aparte/core';
import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';

aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider({
  ...presets.ZAI,
  extraBody: { thinking: { type: 'disabled' } },
}));

extraBody adds fields to every request body, for a vendor’s own switch — here, Z.ai’s thinking field — and extraHeaders adds headers to every request and to the list of models; spreading a preset with extraHeaders replaces its own. The fields the adapter writes itself stay its own: model, messages, temperature, max_tokens, stream, stream_options, seed, tools and tool_choice.

In the chatOn the wire
registered toolstools, with tool_choice: 'auto', or the tool a toolChoice forces
an image part in a messagean image_url part; an assistant turn that calls tools goes as text only
temperature, maxTokens, seedtemperature, max_tokens, seed
the usage of a replystream_options.include_usage, read back as input, output, total and cached tokens
reasoningreasoning_content in the stream, drawn as a reasoning block

The key goes as Authorization: Bearer …, and a key pasted with its Bearer is not doubled.

ts
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 request: AparteChatRequest = { messages: [{ role: 'user', content: 'hi' }], modelId: 'mistral-small-latest', stream: true };

const { path, body, headers } = provider.buildRequest(request);
const response = await fetch(`${provider.defaultEndpoint}${path}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', ...headers, ...provider.authHeaders(process.env['MISTRAL_API_KEY'] ?? '') },
  body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);   // an error body has no events

const stop = new AbortController();
for await (const event of readableToAsyncIterable(provider.parseStream(response.body!), stop.signal)) {
  // text, thinking, tool_use, error, then done with its usage
}

The provider is the wire format alone: build the request, send it yourself, read the stream back as events — in a server route, a worker, another process. It is an OpenAICompatProvider, whose buildRequest, authHeaders and parseText are always there; parseOpenAICompatStream(body) is the same stream reader on its own. readableToAsyncIterable makes the stream something for await reads in every browser.

Ollama and LM Studio speak this format at their /v1 endpoint — for Ollama, rather than its own /api/chat. Local models covers the browser’s permission, llama.cpp, and picking a model.