Skip to content

Troubleshooting

The failures below are the ones that actually happen on a first run, in the order you’re likely to hit them.

This is the #1 first-run failure. With DirectTransport (the default), the browser itself calls http://localhost:1234 (LM Studio) or http://localhost:11434 (Ollama) directly — there is no server in between to add CORS headers for you. If the local server doesn’t send permissive CORS headers, the browser blocks the response and the request fails with a network/CORS error, even though the server logs show it received the request.

The two local presets live in packages/providers/ai/openai-compat/src/presets.ts as presets.LMSTUDIO (http://localhost:1234/v1) and presets.OLLAMA (http://localhost:11434/v1) — see the OpenAI-compatible provider guide for how to register them.

Fix — LM Studio: open the LM Studio server settings (Developer tab) and enable “Enable CORS” on the local server, then restart the server.

Fix — Ollama: start it with the OLLAMA_ORIGINS environment variable set to allow your page’s origin (or * for local development):

Terminal window
OLLAMA_ORIGINS=* ollama serve

On Windows (PowerShell), set it before launching:

Terminal window
$env:OLLAMA_ORIGINS = "*"
ollama serve

Without this, Ollama only accepts requests from a small built-in allowlist of origins and rejects everything else with a CORS error.

”No provider registered” / no model available

Section titled “”No provider registered” / no model available”

Two different symptoms, one root cause: nothing is wired up yet.

“Provider is not registered” — you called AparteConfig.registerAIProvider(...) with a different id than the one selected (or never called it at all). Register the provider and select it before the client sends anything:

import { AparteConfig, DirectTransport, AparteClient } from '@aparte/core';
import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';
AparteConfig.registerAIProvider(createOpenAICompatProvider(presets.OLLAMA));
AparteConfig.setModelConfig({ defaultProvider: 'ollama', defaultModel: 'llama3.2' });
AparteConfig.setTransport(new DirectTransport({ byok: true }));
new AparteClient().start();

“No provider selected” — no defaultProvider/defaultModel is set on AparteConfig.setModelConfig(...) (and no <aparte-model-selector> has picked one yet). Check AparteConfig.hasSelectedModel() — it’s false until both are set. If you want the composer to block sending until a model is chosen (instead of erroring on send), opt into AparteConfig.setRequireModelSelection(true).

Either way, forgetting new AparteClient().start() looks identical to a broken provider from the outside: nothing streams, because nothing is listening for aparte-send. See Wire a real model.

If you see:

[Aparte] DirectTransport is sending the "<provider>" API key straight from the browser —
it is visible to anyone who opens devtools. ...

DirectTransport just sent a real API key from the browser to the vendor, and you didn’t tell it that was intentional. It fires once per page load, the first time a key is attached to a request.

  • Fine to ignore (or silence) when: the key is the end-user’s own (BYOK) or the model runs locally (LM Studio, Ollama) — pass { byok: true } so the warning doesn’t fire at all:

    AparteConfig.setTransport(new DirectTransport({ byok: true }));
  • Not fine when: the key is your server-held vendor key. Anyone with devtools open can read it and use it directly. Switch to BackendTransport (paired with createAparteChatHandler) so the key never reaches the client — see the Backend transport guide.

Keyless local providers never trigger this warning — there’s no key to expose.

Every failure that reaches the UI — a bad request, a rate limit, a network drop, an unregistered provider — is normalized to an AparteError (packages/core/src/types/errors.ts):

class AparteError extends Error {
constructor(
message: string,
code: AparteErrorCode,
data?: Record<string, unknown>,
originalError?: unknown,
httpStatus?: number,
);
static from(error: unknown, defaultCode = AparteErrorCode.UNKNOWN_ERROR, defaultStatus?: number): AparteError;
}

AparteErrorCode is a flat enum grouped by who’s responsible:

CodeMeaning
CONFIG_NO_PROVIDERNo provider selected.
CONFIG_MISSING_KEYThe selected provider isn’t registered (or its key is missing).
CONFIG_INVALID_MODELThe selected model id isn’t valid for the provider.
USAGE_RATE_LIMITVendor rate limit (HTTP 429).
USAGE_CONTEXT_EXCEEDEDContext window exceeded (HTTP 400).
USAGE_BAD_REQUESTMalformed request (HTTP 400).
NET_OFFLINEThe client is offline.
NET_TIMEOUTThe request timed out.
NET_ERRORGeneric network failure.
PROVIDER_ERRORVendor-side error (HTTP 5xx).
PROVIDER_UNAVAILABLEVendor service unavailable (HTTP 503).
PROVIDER_POLICYRejected by the vendor’s moderation/policy.
UNKNOWN_ERRORAnything uncategorized — AparteError.from’s default.

How it surfaces: AparteClient catches whatever the transport/provider throws, wraps it with AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR) (vendor errors don’t already arrive pre-classified, so most surface as UNKNOWN_ERROR unless a provider adapter throws a more specific one), renders it as the message’s error segment (content = error.message, details = error.code), and dispatches an aparte-message-error CustomEvent on the target element with { messageId, error }error is the full AparteError, so error.code, error.data, error.httpStatus and error.originalError are all available to a listener:

document.querySelector('aparte-chat')?.addEventListener('aparte-message-error', (e) => {
const { error } = (e as CustomEvent).detail;
console.error(error.code, error.message);
});

Customize what the error segment looks like with AparteConfig.setErrorRenderer rather than registering a segment renderer for error yourself.