Skip to content

Failed to resolve module specifier "@aparte/core"

Section titled “Failed to resolve module specifier "@aparte/core"”

The page imports @aparte/core by name with no bundler in front of it, and a browser cannot resolve a bare name on its own. Use a bundler — Vite, Next, Astro, esbuild, any of them — as every snippet here assumes, or load the packages from a CDN by URL, as in From a CDN.

The server’s log shows the request, and the reply ends in an error such as Failed to fetch (code NET_ERROR): the browser called the local server itself — http://localhost:1234 for LM Studio, http://localhost:11434 for Ollama — and refused the answer, because the server did not allow the page’s origin.

  • LM Studio: turn on Enable CORS in the server settings, in the Developer tab, then restart the server.
  • Ollama: start it with OLLAMA_ORIGINS set to the page’s origin, or * while you develop.
bash
OLLAMA_ORIGINS=* ollama serve
powershell
$env:OLLAMA_ORIGINS = "*"
ollama serve

If the Ollama app is running, quit it first, or set OLLAMA_ORIGINS as a user environment variable and restart the app.

Local models has the whole setup for both, and for llama.cpp.

”Provider is not registered”, “No provider selected”

Section titled “”Provider is not registered”, “No provider selected””
[AparteClient] Send dropped: no model is selected. …
ts
import { aparteGlobalConfig, AparteClient, AparteDirectTransport } from '@aparte/core';
import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';

aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider(presets.OLLAMA));
aparteGlobalConfig.setModelConfig({ defaultProvider: 'ollama', defaultModel: 'llama3.2' });
aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));
new AparteClient().start();

Neither error appears in the chat: the person’s message gets no reply, the console warns, and aparte-message-error carries the code. Provider is not registered: the selected defaultProvider names an id no provider was registered under. No provider selected: nothing chose a model — no setModelConfig, and no model selector has picked one; aparteGlobalConfig.hasSelectedModel() is false until one is. A provider registered alone with a single model is selected by itself; for a list fetched from the server, <aparte-model-selector auto-select persist> picks one. To keep the composer from sending until a model is chosen, aparteGlobalConfig.setRequireModelSelection(true).

A page that never calls new AparteClient().start() looks the same from the outside: nothing streams, because nothing answers the send.

”Sending the API key straight from the browser” warning

Section titled “”Sending the API key straight from the browser” warning”
[Aparte] AparteDirectTransport is sending the "<provider>" API key straight from the browser —
it is visible to anyone who opens devtools. …

AparteDirectTransport sent a real key from the browser, and says so once per transport. When the key is the person’s own, or the model is local, say so and the warning stops: new AparteDirectTransport({ byok: true }); a keyless local provider never triggers it. When the key is yours, anyone with devtools can read and spend it: send the requests through your server instead.

No renderer claims that segment’s type.

  • A type of yours: register a renderer for it with registerSegmentRenderer, or give the segment a fallback, a sentence drawn as text when nothing claims it. A custom segment’s subType is not a key of its own: one renderer answers every custom segment and reads its subType (Custom segment types).
  • A built-in type — text, thinking, code, tool_call, error — means the built-in renderers were declined, with new AparteClient({ autoRegister: false }). Drop the option, or register what you need yourself.

The retry, edit or ⓘ buttons are not there

Section titled “The retry, edit or ⓘ buttons are not there”
ts
import { aparteGlobalConfig } from '@aparte/core';

aparteGlobalConfig.setBubbleActions({ retry: true, edit: true });     // you run an AparteClient
aparteGlobalConfig.setBubbleActions({ feedback: true, info: true });  // you answer these events
aparteGlobalConfig.setHostHandlers({ attachmentPreview: true });      // you open it on aparte-attachment-preview

Only copy is on by default: the others need something outside the chat to answer them, so they stay off until you say it is there. ⓘ also needs the reply’s token usage: it shows on a reply whose provider reported usage. The bar hides while a reply streams, and a bar with no action in it is not drawn at all. Buttons lists each one.

An empty toolbar is hidden: it carries data-empty while it holds nothing visible, and whitespace or comments do not count. If you put a control in it and still see data-empty, either the control never arrived — a mistyped slot name, a framework that rendered nothing — or it hides itself: a child with hidden or data-empty does not count, and <aparte-context> stays data-empty until the first reply reports its usage. A toolbar that shows but is unstyled, with your control at the start instead of the end, means the stylesheet did not load: import @aparte/core/styles.css (The composer toolbar).

ts
document.querySelector('aparte-chat')?.addEventListener('aparte-message-error', (event) => {
  const { error } = event.detail;   // an AparteError
  console.error(error.code, error.message, error.httpStatus);
});

A failure that reaches the chat — a refused key, a rate limit, a dropped connection — is an AparteError: its message, a code, the httpStatus when there was one, its data and the originalError. The message shows it as an error, and the chat sends aparte-message-error with { messageId, error }. A missing or unregistered provider is the exception: the send is refused before a reply exists, so nothing appears in the chat and only the event reports it. A vendor’s HTTP status sets the code:

Statuscode
400USAGE_BAD_REQUEST
401, 403CONFIG_INVALID_KEY
408NET_TIMEOUT
429USAGE_RATE_LIMIT
503PROVIDER_UNAVAILABLE
any other 5xxPROVIDER_ERROR

A network failure is NET_ERROR, or NET_OFFLINE when the browser says it is offline; a timeout is NET_TIMEOUT; no provider selected is CONFIG_NO_PROVIDER, and one not registered CONFIG_MISSING_KEY. Anything else is UNKNOWN_ERROR — except under a runner that calls the model elsewhere (createAgentStreamRunner, an AG-UI runner), where a failed or cut run, and a status the table does not name, is PROVIDER_ERROR. AparteError.from(error) turns any error into one by the same rules, and AparteError.codeForStatus(status) is the table above, for a provider of yours. To change how the error looks, use aparteGlobalConfig.setErrorRenderer.