Dokümanlar menüsü
Otomatik REST API + OpenAPI + SSE
createGnl yapılandırmasını tek satırda durable HTTP API'ye çevirir: /agents/:name/run|resume|stream (SSE), /workflows, /runs, /usage ve üretilmiş /openapi.json.
Ne işe yarar / ne zaman kullanılır#
Elinde zaten bir createGnl yapılandırması (agent'lar + workflow'lar + storage) varsa, bunu dışarıya bir HTTP API olarak açmak için endpoint'leri elle yazman gerekmez. createRestApi config'i okuyup çalıştırma, resume, SSE streaming, run geçmişi ve kullanım (usage) uçlarını otomatik üretir — hepsi runDurable üzerinden aktığı için exactly-once ve durability garantileri bedava gelir.
Tipik senaryo: bir Hono uygulamasına app.mount('/api', createRestApi(config, opts)) ile bağlayıp, bir istemciyi (web, CLI, başka bir servis) doğrudan bu uçlara konuşturmak. Auth, çok organizasyonluluk ve bütçe/kota opsiyonel — vermezsen uçlar açık ve tek organizasyonlu çalışır (aşağıda örneklenmiştir).
Kurulum / import#
Paket @gnldev/server; journal/storage için @gnldev/durable ve alt-export'u @gnldev/durable/sqlite kullanılır.
npm install @gnldev/server @gnldev/durable honoimport { Hono } from 'hono';
import { createRestApi } from '@gnldev/server';
import { createGnl } from '@gnldev/durable';
import { SqliteStorage } from '@gnldev/durable/sqlite';
const storage = new SqliteStorage('runs.db');
const config = {
storage,
agents: {
starwars: { model: buildModel(), system: SYSTEM, tools: makeSwapiTools(), maxSteps: 4 },
},
};Adım adım kullanım#
Config hazır olunca createRestApi(config, opts) bir Hono router döner; bunu istediğin bir yola bağlarsın (ör. /api).
const app = new Hono();
app.mount('/api', createRestApi(config, { title: 'SWAPI Free', auth }));
// POST /api/agents/starwars/run {"runId":"demo-1","prompt":"..."}
// POST /api/agents/starwars/stream -> SSE (text-delta/tool-call/tool-result/interrupt/done)
// GET /api/usage -> token/maliyet raporuÇalıştırma isteğinde runId zorunludur — bu, exactly-once garantisinin idempotency anahtarıdır; aynı runId ile tekrar POST atarsan tamamlanmış tool çağrıları yeniden çalışmaz.
curl -s -X POST http://localhost:3001/api/agents/starwars/run \
-H 'content-type: application/json' \
-d '{"runId":"demo-1","prompt":"Who is Luke Skywalker?"}'Akış istiyorsan aynı gövdeyle /agents/:name/stream uçuna POST at — yanıt bir SSE akışıdır (text-delta, tool-call, tool-result, interrupt, done event'leri). Bir tool insan onayı için askıya alındıysa (interrupt), aynı runId ile /agents/:name/resume uçuna { runId, approvals } gönderilerek devam ettirilir — girdi (prompt/messages) journal'dan okunur, tekrar vermene gerek yok.
Kullanım/maliyet raporu için GET /usage:
curl -s http://localhost:3001/api/usage
# {"org":null,"usage":{...},"limit":null,"exceeded":false}Üretilen OpenAPI 3.1 şeması GET /openapi.json üzerinden servis edilir — kayıtlı her agent ve workflow için run/resume/stream yolları otomatik eklenir; bunu Swagger/Redoc gibi araçlara doğrudan verebilirsin.
Node sunucuları ve Next.js#
createRestApi bir fetch işleyicisi döndürür. Node'da onu @gnldev/server/node'daki toNodeHandler ile bağlayın:
import { createServer } from 'node:http';
import { toNodeHandler } from '@gnldev/server/node';
express().use('/api', toNodeHandler(api)); // Express
await fastify.register(middie); fastify.use('/api', toNodeHandler(api)); // Fastify + @fastify/middie
koa.use(c2k((req, res, _next) => toNodeHandler(api)(req, res))); // Koa + koa-connect
createServer(toNodeHandler(api)).listen(3000); // node:http
nestApp.use('/api', toNodeHandler(api)); // Nest (Express or Fastify)Canlı sunucularda ölçülmüş ve Express 5, Fastify 5 ile Koa 3'ü ayağa kaldıran bir testle korunan üç kural: rota olarak değil ara katman olarak bağlayın (Fastify'da rota, JSON ayrıştırıcı gövdeyi okuduktan sonra çalışır ve köprü 500 body_consumed_upstream döner); istek gövdesini ayrıştıran her şeyden önce bağlayın; ve Koa'da üçüncü parametreyi yazın — koa-connect fn.length'e bakar, iki parametreyle Koa önce kendi 404'ünü döner.
Next.js köprü istemez: bir route handler Request alır, Response döner. Tek bir catch-all rota, klasörünün altındaki her isteği klasör öneki kesilmiş olarak API'ye verir — next.config'e bir şey eklemek gerekmez:
// app/api/[...path]/route.ts
import { createRestApi } from '@gnldev/server';
import { PostgresStorage } from '@gnldev/durable/postgres';
const api = createRestApi({ storage: new PostgresStorage({ connectionString: process.env.DATABASE_URL }), agents: {} });
function handle(req: Request): Response | Promise<Response> {
const url = new URL(req.url);
url.pathname = url.pathname.replace(/^\/api(?=\/|$)/, '') || '/';
return api.fetch(new Request(url, req));
}
export { handle as GET, handle as POST, handle as PUT, handle as PATCH, handle as DELETE, handle as OPTIONS };Next.js 16.4'te next build ve next start ile ölçüldü: ajan listesi, bir çalıştırma ve server-sent events olarak okunan bir /stream, bellekte ve Postgres'te. Bu Node çalışma zamanıdır (route handler'ın varsayılanı); Edge çalışma zamanı denenmedi, depolar Node ister.
Kesilen bir model akışı cevap değildir#
Bir çalıştırmanın bitişi, model akışının söylediğinden okunur, varsayılmaz. finish parçası olmadan duran ve bir iptal, bir drain ya da bir zaman aşımının durdurmadığı bir akış, çalıştırmayı run_unfinished koduyla failed bitirir — completed değil — ve POST /agents/:name/run 502 cevaplar. /agents/:name/stream'de 200 zaten gönderilmiştir, bu yüzden kodu error olayı taşır. Bir executor'ın close()'da durdurduğu çalıştırma ise released biter: başka bir süreç onu sürdürür, başarısız olmamıştır.
400 değil 502'dir, çünkü istek sağlamdı: kesinti çağıranın yukarısında oldu ve istemcinin yeniden deneme mantığı 400'ü "asla yeniden deneme" diye okur, ki bu tam tersidir. Aynı runId'yi yeniden sürün. Kesilen adım için tamamlanmış hiçbir şey kaydedilmez, dolayısıyla model yeniden çağrılır ve tamamlanan her adım journal'dan yeniden oynatılır; bir tool çağrısından sonra kesilen adım, journal'lanmış çağrısını yeniden oynatır, yani tool bir kez çalışır. Yeni bir runId her şeyi yeniden çalıştırırdı, etkisi olmuş bir tool çağrısı dahil. Kesintiden önce akan metin kaybolmaz: çalıştırmanın olay günlüğündedir.
Kesinti sayılanlar: hiç finish parçası olmaması ya da akışın kendisinin bozulması; interrupted bir finishReason (@ai-sdk/openai 4.0.100 ve @ai-sdk/anthropic 4.0.86'dan itibaren bildirilir); v2 olmayan bir modelde sağlayıcı nedeni ve kullanım bilgisi olmayan bir other bitişi; ya da bir adaptörün akışın bitiş nedeni olmadan bittiğini bildirmesi (@ai-sdk/openai-compatible). Bu, motorun okuduğu sinyallerin listesidir, her kesintinin yakalanacağı sözü değil: adaptör varsayılan finish'ini gönderdiğinde bir v2 modelin kesintisi ve interrupted'dan eski bir adaptörde bir proxy kullanım bilgisini zaten gönderdikten sonraki kesinti bir bitiş gibi okunur ve 200 cevaplanır. wrapLanguageModel arkasındaki bir v2 model kesilmediği hâlde kesilmiş okunabilir; modeli sarmadan kullanın ya da kullanım bilgisi bildirmesini sağlayın.
Alt sınırlar da onunla birlikte yükseldi: ai 7.0.149, @ai-sdk/openai 4.0.100, @ai-sdk/anthropic 4.0.86 ve @ai-sdk/provider 4.0.32, kesilen akışı interrupted olarak bildiren ilk sürümlerdir. Her akışı finish_reason olmadan bitiren OpenAI uyumlu bir uç nokta artık her denemede run_unfinished cevaplar, eskiden 200 cevaplardı — yeniden deneme aynı gövdeyi alır, bu yüzden sunucuyu ya da önündeki gateway'i düzeltin. Tekrarlandığında nelere bakılacağını anlatan tam kayıt docs/errors/run_unfinished.md.
API referansı#
createRestApicreateGnl config'inden Hono router üretir: /agents/:name/run|resume|stream, /agents, /workflows/:name/run, /workflows, /runs, /runs/:id, /usage, /openapi.json.
RestApiOptionscreateRestApi ikinci argümanı: title (OpenAPI başlığı), auth (opsiyonel AuthProvider/ReadWriteAuth), org (opt-in çok organizasyonluluk), budgets (bütçe/kota fallback limitleri).
buildOpenApiAgent ve workflow adı listesinden OpenAPI 3.1 şeması üretir; createRestApi bunu /openapi.json ucunda otomatik çağırır, doğrudan da import edilebilir.
pipeAgentStreamstreamDurable sonucunu (AI SDK StreamTextResult) SSE'ye pompalar: text-delta/tool-call/tool-result/error/interrupt/done event'leri.
interruptsFromStepsTamamlanmış step listesinden askıya alınmış tool çağrılarını (Interrupt[]) çıkarır — runDurable'daki suspend mantığıyla aynı; pipeAgentStream bunu stream sonunda interrupt event'i için kullanır.
toNodeHandlertoNodeHandler(handler) — node:http ve onun üstüne kurulu her çatı (Express, @fastify/middie ile Fastify, koa-connect ile Koa, Nest) için (req, res). Gövdeyi önce başka bir şey okuduysa 500 body_consumed_upstream döner.
streamDurable@gnldev/durable'dan gelir (createRestApi.gnl.stream() içinde dolaylı kullanılır): durable+streaming çalıştırma — aynı exactly-once/suspend garantileri, sonuç fullStream olarak akar.
RunUnfinishedErrorrun_unfinished'ın arkasındaki hata: model akışı finish parçası olmadan bitti ve bu tarafta hiçbir şey onu durdurmadı. 502 cevaplanır.
roleAuth (@gnldev/auth) ile bir auth geçmen önerilir — GET uçları read, POST uçları write yetkisi ister.runId zorunludur — bu exactly-once garantisinin anahtarıdır. Aynı runId ile tekrar istek atmak yeni bir iş DEĞİL, var olanın devamı/tekrarı sayılır (tamamlanmış tool'lar yeniden çalışmaz; askıdaki iş varsa bütçe kapısı da atlanır).