@molecule/app-ai-chat-http
Provider bond · ai-chat · App (browser) · v1.3.3 · Apache-2.0
HTTP/SSE AI chat provider implementation
npm install @molecule/app-ai-chat-httpnpm · Source on GitHub · Implements @molecule/app-ai-chat
How it works
@molecule/app-ai-chat-http is a provider bond on the app (browser) side: it implements the ai-chat core interface (@molecule/app-ai-chat) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
// npm install @molecule/app-ai-chat-http --workspace=app
import { setProvider, requireProvider } from '@molecule/app-ai-chat'
import { createProvider } from '@molecule/app-ai-chat-http'
setProvider(createProvider()) // same-origin base URL by default
const chat = requireProvider()
const config = { endpoint: '/api/ai/chat' } // YOUR backend route (SSE) — the AI key lives there
await chat.sendMessage('Summarize my open orders', config, (event) => {
if (event.type === 'text') appendTokens(event.content) // streamed chunks
if (event.type === 'error') showError(event.message)
})
const history = await chat.loadHistory(config)Works with: @molecule/app-i18n
Reference
HTTP/SSE AI chat provider for molecule.dev.
Quick Start
// npm install @molecule/app-ai-chat-http --workspace=app
import { setProvider, requireProvider } from '@molecule/app-ai-chat'
import { createProvider } from '@molecule/app-ai-chat-http'
setProvider(createProvider()) // same-origin base URL by default
const chat = requireProvider()
const config = { endpoint: '/api/ai/chat' } // YOUR backend route (SSE) — the AI key lives there
await chat.sendMessage('Summarize my open orders', config, (event) => {
if (event.type === 'text') appendTokens(event.content) // streamed chunks
if (event.type === 'error') showError(event.message)
})
const history = await chat.loadHistory(config)
Type
provider
Installation
npm install @molecule/app-ai-chat-http @molecule/app-ai-chat @molecule/app-i18n
API
Interfaces
HttpChatConfig
Configuration for http chat.
interface HttpChatConfig {
/** Base URL for API requests. Defaults to '' (same origin). */
baseUrl?: string
/** Custom headers to include in requests. */
headers?: Record<string, string>
}
Classes
HttpChatProvider
HTTP/SSE-based implementation of ChatProvider. Sends messages via POST to a backend
endpoint and reads SSE (Server-Sent Events) streams for real-time AI responses.
Functions
createProvider(config)
Creates an HttpChatProvider instance with optional base URL and custom headers.
function createProvider(config?: HttpChatConfig): HttpChatProvider
config— HTTP-specific chat configuration (base URL, headers).
Returns: An HttpChatProvider that communicates with the backend via HTTP/SSE.
Constants
provider
Pre-instantiated provider singleton.
const provider: HttpChatProvider
Core Interface
Implements @molecule/app-ai-chat interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/app-ai-chat'
import { provider } from '@molecule/app-ai-chat-http'
export function setupAiChatHttp(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/app-ai-chat^1.0.1@molecule/app-i18n^1.0.1
Runtime Dependencies
@molecule/app-ai-chat@molecule/app-i18n
POSTs each message to YOUR backend chat endpoint (config.endpoint, a RELATIVE path like
/api/ai/chat on the app's baseUrl) and reads the reply as an SSE stream — it does NOT talk
to an AI provider directly and holds NO AI key. Point endpoint at your own API, where the
provider key + @molecule/api-ai live; auth rides the session via the HTTP client
(cookie/bearer), so never attach a provider key or an absolute AI-provider URL here. See
@molecule/app-ai-chat for the safe-render rules.
Server contract (all on the ONE config.endpoint route): POST
{ message, model?, attachments?, resume?, suppressUserMessage?, automatic?, userInitiated? } → SSE data: <ChatStreamEvent JSON> lines;
GET → { messages, streaming? } plus any app-specific top-level fields (this
bond only reads messages + streaming; every other field rides through in
provider.lastMeta — e.g. an app that persists an agent mode reads it back as
provider.lastMeta?.mode); DELETE → clear history. Two conventions beyond
that route: a POST answered 409 means "conversation locked, still
streaming" — this bond retries automatically (up to 10 tries, 500 ms
doubling backoff) so return 409 rather than erroring; and Stop/unload
aborts POST to <endpoint>-abort (suffix on the pathname, query kept)
with { conversationId?, userInitiated? } via sendBeacon.
The 409 and the resume flag work together: a resume send (resume: true,
empty body) carries Accept: text/event-stream, so a server whose turn is
STILL RUNNING can attach the request to that live turn as a stream
subscriber (an attached frame, then the turn's remaining frames) instead
of 409ing. NEW sends deliberately do NOT send that Accept header — the
attach path never reads the request body, so a new message must keep the
409-retry path to run after the current turn; serving it the attach
response would silently drop it.
abortOnServer(), isServerStreaming, and lastMeta are extensions on
HttpChatProvider beyond the core ChatProvider type. loadHistory()
returns [] on HTTP errors but REJECTS on network failure; wrap it.
A non-ok response becomes an error event, and a JSON body's error,
limitType, requiresSignup, billingAction and upgradeTier are copied
onto it — so a limit refusal (402/429/403) reaches the UI with both the rule
that fired (limitType) and the remedy the backend resolved
(billingAction, e.g. add_funds vs add_payment_method vs upgrade,
with upgradeTier: null meaning there is no higher plan). Send those fields
from your API and the client's call-to-action is yours to decide; omit them
and the client can only guess.
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- Sending a message renders it in the thread and a streamed assistant reply appears incrementally (visible tokens while generating — not a frozen UI that dumps one blob).
- The reply flows through the app's OWN backend: the browser's network log shows no direct calls to an AI provider and no provider key anywhere client-side.
- Model output renders as sanitized markdown — a reply containing HTML
or
<script>displays as text and never executes. - If the app claims conversation persistence, reloading restores the thread history.
- A backend failure (endpoint down, missing API key) surfaces a readable, actionable error — not an infinite spinner.
- Sending again while a reply streams is handled sanely (queued, blocked, or parallel — never corrupted/interleaved text).