@gnldev/chat-adapter has 384 recorded weekly downloads on npm.
About @gnldev/chat-adapter
@gnldev/chat-adapter
Compatibility with the Vercel AI SDK's UI layer: run a durable agent on the server, render it with
useChat on the client.
Install
Install:
pnpm add @gnldev/chat-adapter— or use it from a repo clone:pnpm install && pnpm -r build.
npm i @gnldev/chat-adapter
On the REST API (recommended)
Mount the chat format on @gnldev/server's REST API. The API's auth decides who the caller is, which
organization they are in and whose run this is — the same gates as every REST route — and there is
no second identity resolver to write:
import { createRestApi } from '@gnldev/server';
import { chatSurface } from '@gnldev/chat-adapter';
import { roleAuth } from '@gnldev/auth';
import type { CreateGnlConfig } from '@gnldev/durable';
declare const config: CreateGnlConfig;
export const api = createRestApi(config, {
auth: roleAuth({ endUsers: { secret: process.env.GNL_END_USER_SECRET!, orgId: 'acme' } }),
surfaces: [chatSurface()], // POST /agents/:name/chat — useChat({ api: '/agents/pay/chat' })
cors: { origins: ['https://app.example.com'] }, // if the browser calls this API directly
});
A request with no valid credential is refused before the model runs. An end user's turn is filed
under that user and their organization, shows up in their GET /runs, and is visible to their
organization's staff in Studio. chatSurface({ path }) changes the mount path.
Sending the user's token from useChat. With end users, a request without a token is a 401, so
the browser attaches one on every request. headers may be an async function, so each request asks
your token route for a current one:
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { tokenFrom } from '@gnldev/client';
const getToken = tokenFrom('/gnl-token'); // your app's route (subjectTokenEndpoint in @gnldev/auth)
const chat = useChat({
transport: new DefaultChatTransport({
api: 'https://api.example.com/agents/pay/chat',
headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
}),
});
Chat route (standalone)
For a host that authenticates the request itself (a session cookie — with SameSite —
or a verified token) and tells the route who is calling through identify. In production it refuses to start without identify. Your
identify may answer undefined for a visitor with no session: that visitor is served as anonymous,
with the checks below.
import { createChatRoute } from '@gnldev/chat-adapter';
import type { Principal } from '@gnldev/auth';
// Your session lookup: who this request is, as YOUR server established it.
declare function userOf(req: Request): Promise<string | undefined>;
app.route('/api', createChatRoute({ gnl }, {
identify: async (req): Promise<Principal | undefined> => {
const id = await userOf(req);
return id ? { kind: 'subject', id, roles: [] } : undefined;
},
}));
// POST /api/agents/:name/chat — useChat({ api: '/api/agents/pay/chat' }) works unchanged.
The route streams back in the AI SDK's UI-message format, so an existing useChat frontend works
unchanged — while the run behind it is journaled, replayable, and its side effects are
at-most-once.
You normally do NOT need resolveRunId: the default derivation ${body.id}:${lastMessage.id} gives
one durable run PER TURN (a network retry of the same turn replays; a new turn runs fresh).
Anti-pattern to avoid: resolveRunId: (_c, body) => body.id — useChat's body.id is stable for the
WHOLE conversation, so every later turn would replay turn 1 from the journal forever. What that
derived string is — a name the engine hashes into an id, or the id itself — depends on whether the
route can name the user; see the idempotency contract.
A request with no identity: what the route refuses
A request carries an identity when identify resolved a caller for it. One that did not — the route has
no identify, or identify answered nothing for it (an anonymous visitor; identify: () => undefined
for everyone) — has no identity to check, so it is refused what a REST API with no auth refuses — the
same two checks, from the same @gnldev/auth functions (doorGate):
- a request addressed by a name other than
localhost, a*.localhostname or an IP address → 403host_not_allowed. That is what a DNS-rebinding page sends; it is same-origin to the browser, so without this it could replay a run and read the thread back. - a state-changing request a page on another site sent (
Sec-Fetch-Site: cross-site) → 403cross_site_refused. A plain<form>on any site could otherwise run the model and write into a conversation.
Nothing changes on this machine: localhost, 127.0.0.1, a LAN IP, a UI on another localhost port (or
on 127.0.0.1 calling localhost), curl or a mobile app with no Origin, and an in-process fetch are
answered. A name of your own (a tunnel, Codespaces, a Docker service) is one line:
createChatRoute(…, { allowedHosts: ['.ngrok-free.app'] }), or GNL_ALLOWED_HOSTS=.ngrok-free.app in the
environment. A widget embedded on other sites with no sign-in is one line too:
createChatRoute(…, { allowCrossSite: true, allowedHosts: ['chat.example.com'] }) — allowCrossSite
lifts the cross-site check only. A request identify resolved a caller for gets neither check.…
Description from the npm listing.
- ai
- agent
- llm
- typescript
- ai-sdk
- durable
- exactly-once
- usechat
- ui-stream
README badge
Show your rank in a README or on your website. The badge updates automatically.
Charts
Images: 30d · 1y · all time · 1600×900 dark
Adoption over time
7/30/90-day growth
Ranking history
Listing placement
Listing placement
Filled points are observed values; hollow points come from Internet Archive snapshots of the listing page. Dashed segments span periods with no observations — values between points are not interpolated.
Each band is one page of 20. Weekly downloads on a linear scale. Placement is projected from weekly downloads, so it can differ from the marketplace's own sort order. Hover a dot to see the listing.
Recent observations
Full history (1)Similar npm AI SDKs plugins
All 20 @gnldev/chat-adapter alternativesData source
Weekly downloads (trailing 7 complete days) come from the official npm downloads API. Downloads include CI and mirrors and are not equivalent to installs or users. npm publishes no ratings.
Listing: https://www.npmjs.com/package/@gnldev/chat-adapter. First seen Oct 22, 2026; last successful collection Oct 22, 2026. See the methodology.