Implement Peft LLM chat in this online/HTML game.
Prerequisite: implement Hosting basics if missing. The game must run in a browser without requiring Windows. Room hosting and multiplayer are not prerequisites.
Peft supplies private language-model access to approved, signed-in members while the game is hosted on Peft. The trusted parent wrapper handles authentication, CSRF, release authorization, model discovery, queuing, and cancellation. The sandboxed game receives no API key, account cookies, signed context, or private backend address. Do not fetch Peft API routes or a private model host directly from the game.
Feature detection and API:
const service = window.PEFT?.llm;
if (!service) {
// Show an honest unavailable state; keep non-AI gameplay working.
}
const catalog = await service.models({signal});
// catalog.models: [{id, name, family, description}, ...]
// catalog.limits: messages, characters, output_tokens,
// pending_per_member_game, global_waiting,
// queue_timeout_seconds, request_timeout_seconds
const completion = await service.chat(
{
model: selectedModelId,
messages: [
{role: "system", content: "You are a lighthouse keeper. Stay in character."},
{role: "user", content: "What quest can you offer?"}
],
max_tokens: 1024
},
{signal, onStatus, onToken}
);
const answer = completion.choices[0].message.content;
models() and chat() return Promises. All options are optional. signal is an AbortSignal; onStatus receives an object such as {state: "queued", position: 2, request_id: "..."} or states preparing/thinking/replying. onToken receives a visible-text string. Callbacks may return Promises: Peft waits for consumption and fails a consumer that stops responding. The result uses a text Chat Completion shape: id, object, created, model, choices, usage, plus Peft metrics. metrics includes queue_ms, first_token_ms, first_answer_ms, total_ms, output_token_limit, truncated, and any reported token rate. Not all upstream token/rate fields are always available. Times before the first backend token combine model loading, prefill, and generation; they are not an isolated model-load measurement.
Supported request fields are model, messages, and optional max_tokens. Use alternating user/assistant text messages, optionally preceded by exactly one system message. Finish each request with a user message. Include the relevant history on every request; Peft keeps no server-side conversation. On success append the visible assistant reply to that conversation. Do not duplicate the system message on successive turns. Do not mix independent NPC/player conversations or silently reuse old responses after changing persona. Images, audio, tools, arbitrary URLs, provider headers, and sampling controls are not supported by this helper.
Discover model choices rather than hard-coding availability. Fast, Default, and Strata may be offered; other benchmarked models may be hidden. Never substitute another model silently. Prefer one model per game: switching can take tens of seconds, particularly into or out of Strata. The configured upstream router handles model loading/exclusivity; games must not call load/unload controls. Peft runs one generation at a time and schedules members fairly. Native reasoning stays enabled where applicable: thought text is discarded, but its time and tokens count, so thinking can consume an output budget before any visible answer.
Pre-load the chosen model when the game starts, during its loading/setup screen, BEFORE enabling AI-dependent gameplay. Do not leave an approximately eight-second (or longer, depending on the switch) model load until the first player interaction. models() only discovers availability; it does NOT load a model. Use one small, real startup chat request to warm the selected model through the normal game queue, and await completion before marking AI ready:
async function prepareGameAI(service, selectedModelId, signal) {
const catalog = await service.models({signal});
if (!catalog.models.some(model => model.id === selectedModelId)) {
throw new Error("The selected game model is unavailable.");
}
await service.chat(
{
model: selectedModelId,
messages: [{role: "user", content: "Warm up for this game."}],
max_tokens: 1
},
{signal, onStatus: status => { loadingStatus.textContent = status.state; }}
);
return selectedModelId;
}
Use the returned model ID for the rest of the game. Discard this startup completion; it is not an NPC/player turn and must not enter conversation history. An empty or token-limited warm-up reply is expected: it still loads the model without disabling native reasoning. Show loading/queued/failure states and allow cancellation; never report AI ready after a failed warm-up. In multiplayer, let the authoritative host warm once and signal readiness to peers instead of warming from every client. Keep non-AI gameplay usable when AI is unavailable. Warming is not a permanent GPU reservation: another game's requests or external backend clients may still change the loaded model. Peft disables the standalone "Try a model" tester while any advertised lobby is active to avoid test-driven switching during games.
The small limits displayed in the AI test pane apply only to that tester, not hosted games. Games have separately configurable bounds, discoverable through catalog.limits. Initial game defaults allow 121 conversational messages plus an optional system prompt, 64,000 total input characters, up to 8,192 output tokens including reasoning, 32 outstanding requests per member/game, 128 waiting requests globally, a 30-minute queue wait, and a 10-minute generation deadline. These are resource-safety limits, not billing or cumulative token quotas. They can change: honor discovery rather than copying these defaults into game logic. Large batches can queue and drain; keep client-side pending work bounded too. A full queue rejects with code busy; do not generate automatic duplicate requests.
Streaming example:
const controller = new AbortController();
let visibleText = "";
try {
const result = await service.chat(
{model: selectedModelId, messages, max_tokens: 1024},
{
signal: controller.signal,
onStatus: status => {
statusElement.textContent = status.state === "queued"
? `Waiting${status.position ? ` (position ${status.position})` : ""}`
: status.state;
},
onToken: text => {
visibleText += text;
answerElement.textContent = visibleText;
}
}
);
if (result.metrics.truncated) {
statusElement.textContent = "Output limit reached; reply may be incomplete.";
}
} catch (error) {
statusElement.textContent = error.name === "AbortError"
? "Cancelled"
: `AI unavailable: ${error.message}`;
}
// Wire a Stop button and game/scene teardown to controller.abort().
For a non-streaming UI, omit onToken and render completion.choices[0].message.content after the Promise resolves. Do not replace native reasoning settings. Render all model text with textContent, never innerHTML or execution/eval. Validate structured responses against the game's schema and rules; model output is not authoritative game state. Do not send secrets or sensitive account/player information.
Handle queued/loading states visibly without freezing gameplay. Keep requests independent and correlated to the correct NPC/player. In multiplayer, deliberately choose whether the authoritative host makes one shared request and broadcasts its result, or each approved player makes a separate private request. Do not have every client generate an identical shared reply. Room-report methods do not provide multiplayer networking or authorize extra LLM privileges.
There are no automatic generation retries. Handle sign_in, expired_page, access_denied/access_revoked, busy, queue_timeout, timeout, invalid_model, unavailable/upstream_unavailable, upstream_protocol, cancelled, and queue_cleared errors. Preserve useful user input; let a person choose whether to retry when appropriate. Administrators can clear pending requests in Manage Peft without interrupting the current generation. A cleared job fails explicitly and must not automatically refill the queue.
Pending work and histories are not persisted by Peft. Navigation, reload, disconnection, process restart, and explicit abort can end requests; upstream compute cancellation is best effort. If a browser/game crashes, its work is not replayed later. Standalone launches and the networkless screenshot renderer have no LLM bridge: do not contact production or break game startup. The bridge supports at most 64 local pending calls; use the lower discovered member/game cap for normal batching.
Add focused game tests for missing bridge, model discovery/failure, startup warm-up finishing before AI-dependent gameplay (including failure/cancellation and a reasoning-only warm-up), one system persona across multiple turns, independent conversation histories, queued bursts, status/token rendering, cancellation/scene teardown, timeout/full/cleared queues, hidden/revoked access, truncated or reasoning-only replies, safe text rendering, and multiplayer duplicate prevention. Acceptance: an approved member can use the game's own chat UI through the sandboxed bridge, with no exposed credentials, while all unrelated gameplay and Peft room/victory features still work.