napplet.
← All NAPs

NAP-LLM / PR #134

User-selected language models

Open PR

Full specification from the fetched upstream revision ↗. The document below is rendered from its original Markdown. Follow the latest source on GitHub ↗.

NAP-LLM

User-selected language models

draft

NAP ID: NAP-LLM Domain: llm Web binding (NIP-5D): window.napplet.llm; domain presence signals availability.

Description

NAP-LLM lets a napplet request text generation, optionally with images, through a user-selected model connection owned by the runtime. The napplet owns its prompts, conversation and result validation. The runtime owns connection selection, credentials, disclosure consent and provider access. A connection may use the user's API key, a local model or a runtime-provided service. None grants the napplet direct network access or access to other napplets' conversations.

This interface has no dependency on another NAP. A purpose-built remote service can instead expose an application contract through NAP-CVM. Service checkout, payments, embeddings, audio and video generation are outside this proposal. Tool calls and results are part of the wire so napplets and runtimes can represent agentic turns honestly. The runtime MUST NOT execute a provider-returned tool call through this domain. The napplet owns tool execution and may use a separately authorized capability. The reference to NAP-CVM above is an alternative application contract, not a dependency of this domain.

API Surface

Operation Parameters Result Wire
select optional requirements (Requirements) Model llm.select / llm.select.result
describe handle (text) Model llm.describe / llm.describe.result
generate request (GenerationRequest) Generation; optional Delta updates llm.generate / llm.generate.result, llm.delta
cancel requestId (text) boolean llm.cancel / llm.cancel.result

select asks the user to choose a connection meeting the requirements. The runtime MUST NOT silently substitute a different requested model or downgrade a required capability. Selection does not authorize sending a prompt. describe revalidates a selection without generating output or showing another chooser. A handle becomes invalid when its connection, model, credentials, account or endpoint lifetime changes. The napplet must select again after invalidation.

generate is one stateless request: the supplied messages are the entire conversation for that operation. The runtime MUST NOT append another conversation or silently discard supplied messages, roles or image parts. The display-only thinking exception below does not permit discarding other conversation content. Provider-specific input restrictions MUST produce unsupported before the generation request is sent. Mapping roles and parts into the provider's equivalent format is permitted.

Schemas

Unknown fields in these input records, explicit null, invalid types and invalid encodings MUST produce invalid-request. Optional fields may be omitted.

Requirements

Field Required Type Notes
model no non-empty text Exact provider model identifier; omission allows user choice
images no boolean true requires image input; omitted or false imposes no requirement
json no boolean true requires JSON-object output; omitted or false imposes no requirement
tools no boolean true requires tool-calling support; omitted or false imposes no requirement

Model

Field Required Type Notes
handle yes non-empty text Opaque reference scoped to this authenticated endpoint and user account
name yes text User-facing connection label
model yes non-empty text Selected model identifier
route yes text Human-readable disclosure route, including intermediaries and destination; never credentials
images yes boolean Whether the connection supports image input
json yes boolean Whether it supports JSON-object output
tools yes boolean Whether it supports tool calling
toolChoiceModes yes list of text Supported modes: auto, none, required, tool; empty when tools is false

The handle is not a URL, credential or transferable capability. The runtime MUST NOT accept a napplet-supplied provider URL, key or identity as selection authority. images does not promise every provider supports every placement of an image within a conversation; unsupported combinations must be rejected explicitly.

tools: true MUST support auto and none. toolChoiceModes MUST list exactly the supported modes, without duplicates. tool means named-tool forcing. A valid request for an unlisted mode MUST produce unsupported before provider submission; the runtime MUST NOT substitute another mode.

GenerationRequest

Field Required Type Notes
handle yes non-empty text A live Model handle
messages yes non-empty list of Message Ordered conversation
format no text or json Defaults to text; json requires an object, not an array or scalar
maxTokens no positive integer Output token limit; omission requests no napplet-defined limit. Runtime policy and model limits still apply; runtime policy MAY cap it
temperature no number Sampling temperature; unsupported values MUST produce unsupported
tools no list of Tool Tools the model may call
toolChoice no auto, none, required or ToolChoice How the model selects tools; defaults to auto when tools are present

Tool

Field Required Type Notes
name yes text Portable tool name matching ^[A-Za-z0-9_-]{1,64}$
description no text What the tool does
parameters no map of text to any JSON Schema with root type: "object"; omission means {"type":"object","properties":{},"additionalProperties":false}

parameters uses JSON Schema draft 2020-12. Malformed schemas MUST produce invalid-request.

Tool names MUST be unique within tools. An empty list is equivalent to omission. An explicit toolChoice requires a non-empty tools list; a forced name MUST appear in that list. Violations, invalid names and a parameters value without root type: "object" MUST produce invalid-request. A structurally valid schema or choice unsupported by the connection MUST produce unsupported before provider submission. JSON Schema keywords are not unknown record fields.

auto permits zero or more calls; none prohibits calls; required requires at least one call; a named choice requires at least one call, all to that name. A successfully completed turn violating the selected mode MUST produce provider-error, not a fallback. Length and refusal remain partial outcomes. Historical calls and results MAY be supplied without current tools; this allows continuing a completed exchange without enabling new calls. Without tools, the runtime MUST NOT accept new provider tool calls.

ToolChoice

Field Required Type Notes
type yes tool Forces one named tool
name yes non-empty text Tool name to force

Message

Field Required Type Notes
role yes system, user, assistant or toolResult Role of this message
content yes non-empty list of Part Ordered content parts
toolCallId for toolResult only non-empty text Matches the toolCall part this message answers
toolName for toolResult only non-empty text Name of the tool that produced the result
isError no boolean For toolResult: whether the tool failed; defaults to false

A toolResult message MUST answer exactly one call in the immediately preceding assistant turn. Results MUST immediately follow that assistant message, one per call, in any order, before any other message. Missing, duplicate, orphaned or name-mismatched results MUST produce invalid-request. The runtime MAY regroup results for its provider without changing their meaning. Call ids MUST be unique within the supplied conversation. When a provider omits an id, the runtime MUST synthesize one that remains unchanged in deltas and the terminal result; the napplet MUST reuse it in the subsequent assistant history and matching result. Ids are not credentials.

toolResult content permits only text and image. toolCall and thinking parts are permitted only in assistant messages; isError is permitted only on toolResult. Invalid placements MUST produce invalid-request. A connection that cannot accept images in tool results MUST produce unsupported before submission. If the provider has no native error flag, the runtime MUST represent isError: true by prefixing the result with a text part containing Tool execution failed. followed by the supplied content. It MUST NOT silently discard the error indication.

Part is one of these records:

Field Required Type Notes
type yes text, image, thinking or toolCall Selects the record variant
text for text/thinking only text Text content
mimeType for image only image/png, image/jpeg or image/webp Declared image encoding
data for image only non-empty text Standard padded base64 bytes, no whitespace, data-URI prefix or remote URL
toolCallId for toolCall only non-empty text Stable id matching the later toolResult
name for toolCall only non-empty text Tool name
arguments for toolCall only map of text to any Tool arguments as JSON

thinking is display-only, not reasoning continuation state. The runtime MUST omit it from provider input when assistant history is replayed. If continuing that history requires opaque or signed provider state that this contract cannot represent, the runtime MUST return unsupported before provider submission. It MUST NOT silently restart or degrade the reasoning loop. Providers that return only redacted reasoning MAY omit a thinking part; no placeholder text or provider blob is required. Supporting opaque continuation is outside this draft.

Generation

Field Required Type Notes
content yes list of Part Complete accumulated output; only text, thinking and tool-call parts; may be empty for length or refusal
finish yes stop, length, refusal or toolUse Normal completion, output limit reached, provider refusal, or a tool call
usage no Usage Provider-reported token counts, when available

Usage

Field Required Type Notes
inputTokens yes non-negative integer Reported input tokens
outputTokens yes non-negative integer Reported output tokens
cacheReadTokens no non-negative integer Tokens read from a provider cache
cacheWriteTokens no non-negative integer Tokens written to a provider cache
reasoningTokens no non-negative integer Tokens attributed to reasoning

Only finish: "stop" indicates normal completed output. length and refusal are not completed output. A successfully completed turn containing any toolCall MUST have finish: "toolUse", regardless of the provider's normal stop label; conversely, toolUse MUST contain at least one call. Tool execution MUST wait for an authoritative toolUse result, never a delta. Truncation and refusal MUST NOT be relabelled toolUse: such results MUST contain no toolCall parts, even if some calls completed before another was truncated. Previously streamed call fragments remain provisional and are discarded.

Every completed call's arguments MUST parse as a JSON object, and its name MUST appear in the current tools. Malformed arguments, non-object arguments, unknown names or missing completion markers MUST produce provider-error with no raw provider body echoed. Incomplete arguments on an explicitly truncated/refused turn are discarded under the rule above, not treated as completed calls.

For format: "json", concatenate the text parts of a stop result in order; the concatenation MUST parse as a JSON object. This does not validate the application schema, factual accuracy or suitability. Usage is not a price or settlement receipt; cache and reasoning token counts do not imply a cost.

Delta

Field Required Type Notes
requestId yes non-empty text The originating generation's id
sequence yes non-negative integer Contiguous sequence starting at zero for this generation
event yes start, delta or end Block lifecycle event
block yes text, thinking or toolCall Kind of content block
blockIndex yes non-negative integer Zero-based index of the block within the generation's content
text for delta only text Text fragment, or JSON argument fragment for toolCall
toolCallId for toolCall start only non-empty text Id used by the terminal call
name for toolCall start only text Tool name used by the terminal call

Each streamed block MUST have one start, zero or more delta events and one end, in that order. Blocks MAY interleave; indices and kinds MUST remain stable. Concatenated text fragments MUST equal the final block text. For a completed toolCall, concatenated argument fragments MUST parse to the final arguments object. A runtime MAY serialize a buffered argument object to produce fragments. A block's end does not authorize execution. Error, cancellation, length or refusal MAY terminate a provisional block without end; only successfully completed blocks are required to match the terminal content indices.

Wire Protocol

Messages use the registry's domain.action envelopes. Delivery and endpoint binding belong to the projection; see the web projection.

Type Direction Payload fields
llm.select napplet → runtime id, optional requirements
llm.select.result runtime → napplet id, model or error
llm.describe napplet → runtime id, handle
llm.describe.result runtime → napplet id, model or error
llm.generate napplet → runtime id, request
llm.delta runtime → napplet Delta fields; no separate id
llm.generate.result runtime → napplet id, result (Generation) or error
llm.cancel napplet → runtime id, requestId
llm.cancel.result runtime → napplet id, cancelled (boolean) or error

Request IDs are non-empty text, scoped to the authenticated endpoint. Callers MUST use a fresh ID for every request, including cancellation. A routable, validly correlated request receives exactly one terminal result with its ID and matching result type, unless the endpoint is destroyed. Error results contain error and no success payload. An active duplicate generation ID MUST NOT start another provider request; it receives invalid-request. This does not cancel the original request, whose eventual result may still arrive. IDs are correlation identifiers, not an exactly-once or retry guarantee.

Deltas are optional: a runtime MAY buffer the complete output and send no deltas. When sent, they MUST be ordered and routed only to the requesting endpoint. The terminal result is authoritative; the caller replaces provisional content with the result's content, rather than appending it. No deltas may follow the terminal result. On an error, any provisional content remains incomplete. Slow consumers may receive fewer, larger deltas; the runtime MUST NOT silently drop text or arguments between sequence numbers.

Cancellation and deadlines

cancelled: true means the runtime found an active generation belonging to this endpoint and terminated its delivery with a cancelled error. It also requests provider cancellation, on a best-effort basis. Cancellation while awaiting consent MUST prevent provider submission. An unknown, already terminal or other endpoint's requestId returns cancelled: false, without revealing its existence.

If completion wins the race, its result stands and cancellation returns false. If cancellation wins, the generation receives only the cancellation error; late provider output is discarded. The cancel result and generation result need not arrive in a particular relative order. Account changes, endpoint destruction and revoked authorization MUST cancel affected work and invalidate its handles.

The runtime MUST bound generation time under its policy, including consent wait, and report timeout when that deadline expires. It MUST settle cancellation or timeout without waiting indefinitely for the provider. Neither proves that the provider stopped computing or charging. A caller MUST NOT automatically retry an ambiguous timed-out or cancelled generation. A user-initiated retry uses a new ID and new disclosure authorization; it may incur another charge.

Examples

Choose and inspect a model:

{"type":"llm.select","id":"s1","requirements":{"json":true}}
{"type":"llm.select.result","id":"s1","model":{"handle":"h1","name":"My connection","model":"chosen-model","route":"Local shell → inference provider","images":false,"json":true,"tools":true,"toolChoiceModes":["auto","none","required","tool"]}}
{"type":"llm.describe","id":"d1","handle":"h1"}
{"type":"llm.describe.result","id":"d1","model":{"handle":"h1","name":"My connection","model":"chosen-model","route":"Local shell → inference provider","images":false,"json":true,"tools":true,"toolChoiceModes":["auto","none","required","tool"]}}

Generate, optionally stream, then finish (a buffered runtime omits all deltas):

{"type":"llm.generate","id":"g1","request":{"handle":"h1","messages":[{"role":"user","content":[{"type":"text","text":"Return a JSON object with a greeting."}]}],"format":"json","maxTokens":128,"temperature":0.7}}
{"type":"llm.delta","requestId":"g1","sequence":0,"event":"start","block":"text","blockIndex":0}
{"type":"llm.delta","requestId":"g1","sequence":1,"event":"delta","block":"text","blockIndex":0,"text":"{\"greeting\":\"Hello\"}"}
{"type":"llm.delta","requestId":"g1","sequence":2,"event":"end","block":"text","blockIndex":0}
{"type":"llm.generate.result","id":"g1","result":{"content":[{"type":"text","text":"{\"greeting\":\"Hello\"}"}],"finish":"stop","usage":{"inputTokens":12,"outputTokens":7}}}

Call a tool, answer it, then continue. The assistant turn carries a toolCall part; the next request answers it with a toolResult message. Multiple parallel calls are allowed and MUST each be answered:

{"type":"llm.generate","id":"g2","request":{"handle":"h1","messages":[{"role":"user","content":[{"type":"text","text":"What is the weather in Berlin?"}]}],"tools":[{"name":"get_weather","description":"Look up current weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],"toolChoice":"auto"}}
{"type":"llm.generate.result","id":"g2","result":{"content":[{"type":"toolCall","toolCallId":"call_1","name":"get_weather","arguments":{"city":"Berlin"}}],"finish":"toolUse","usage":{"inputTokens":31,"outputTokens":12}}}
{"type":"llm.generate","id":"g3","request":{"handle":"h1","messages":[{"role":"user","content":[{"type":"text","text":"What is the weather in Berlin?"}]},{"role":"assistant","content":[{"type":"toolCall","toolCallId":"call_1","name":"get_weather","arguments":{"city":"Berlin"}}]},{"role":"toolResult","toolCallId":"call_1","toolName":"get_weather","content":[{"type":"text","text":"18°C and clear"}]}],"tools":[{"name":"get_weather","description":"Look up current weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],"toolChoice":"auto"}}
{"type":"llm.generate.result","id":"g3","result":{"content":[{"type":"text","text":"It is 18°C and clear in Berlin."}],"finish":"stop","usage":{"inputTokens":58,"outputTokens":15}}}

A streamed tool call identifies the call before its argument fragments:

{"type":"llm.delta","requestId":"g2","sequence":0,"event":"start","block":"toolCall","blockIndex":0,"toolCallId":"call_1","name":"get_weather"}
{"type":"llm.delta","requestId":"g2","sequence":1,"event":"delta","block":"toolCall","blockIndex":0,"text":"{\"city\":\"Berlin\"}"}
{"type":"llm.delta","requestId":"g2","sequence":2,"event":"end","block":"toolCall","blockIndex":0}

These events precede the g2 terminal result above; they are not a second call.

Force one named tool with toolChoice:

{"type":"llm.generate","id":"g4","request":{"handle":"h1","messages":[{"role":"user","content":[{"type":"text","text":"Check the weather."}]}],"tools":[{"name":"get_weather","description":"Look up current weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],"toolChoice":{"type":"tool","name":"get_weather"}}}

Cancel a separate active generation, then try again after it has ended:

{"type":"llm.cancel","id":"c1","requestId":"g2"}
{"type":"llm.generate.result","id":"g2","error":{"code":"cancelled","message":"Generation cancelled; provider charges may still apply."}}
{"type":"llm.cancel.result","id":"c1","cancelled":true}
{"type":"llm.cancel","id":"c2","requestId":"g2"}
{"type":"llm.cancel.result","id":"c2","cancelled":false}

Explicit failure and partial completion:

{"type":"llm.generate.result","id":"g5","error":{"code":"unsupported","message":"This connection does not accept image input."}}
{"type":"llm.generate.result","id":"g6","result":{"content":[{"type":"text","text":"Incomplete answer"}],"finish":"length"}}
{"type":"llm.generate.result","id":"g7","error":{"code":"timeout","message":"Generation deadline reached; provider charges may still apply."}}

Error Handling

Error

Field Required Type Notes
code yes text Stable failure category from the table below
message yes text Safe human-readable diagnostic, never a raw provider body or credential-store error
Code Meaning
unsupported Domain unavailable, no compatible connection, or unsupported input/format
invalid-request Malformed input or active duplicate generation ID
selection-expired Unknown, revoked, changed or foreign handle; select again
user-cancelled The user declined selection or disclosure authorization
cancelled Generation cancelled or its endpoint/account authorization revoked
timeout Runtime generation deadline expired; provider effects may be uncertain
quota-exceeded Runtime size, concurrency, rate or usage policy prevented the request
provider-error Provider/transport failure, missing completion marker, or invalid completed output

Unknown error codes MUST be treated as failures. A declared capability does not guarantee a request will be authorized. Runtime limits are policy, not fixed protocol maxima; requests exceeding those limits MUST fail explicitly rather than truncating messages, dropping images or silently changing models.

Shell Behavior

  • The runtime MUST bind each request to the authenticated endpoint and verified napplet identity. It MUST scope handles, results and cancellation to that endpoint and the current user account. Shared backend connections do not weaken this rule.
  • Before submitting each generation, the runtime MUST obtain user authorization identifying the requesting napplet, selected route/model, data being disclosed (including tool definitions and tool results) and potential provider charges. It MUST NOT send prompts, images, tool definitions or tool results before that authorization. A stored API key or model selection is not authorization.
  • Changing the selection while consent is pending requires fresh selection and consent; the runtime MUST NOT silently use the replacement connection.
  • The runtime MUST keep credentials and private connection identifiers outside the napplet, preserve the sandbox, and perform provider communication itself.
  • Domain absence MUST be discoverable without a shell handshake. Napplets with an optional llm integration must retain their other functionality when absent.

Security Considerations

Prompts, images, tool definitions, tool results and output can be sensitive. The runtime MUST NOT broadcast them, include them in unrelated errors, or disclose them to another endpoint/account. It MUST disclose intermediaries as well as the ultimate provider; "local model" does not imply data stays on the user's device when the runtime is hosted.

Tool results re-enter the conversation as model input and MUST be treated as untrusted data, including instructions embedded in them. A tool call is a request, not authorization; the napplet must validate it and obtain any separately required authority before execution.

Model output is untrusted data, never an authorization to execute tools, sign, pay, fetch URLs or change connections. This contract grants none of those powers. A runtime MUST NOT execute provider-returned tool calls through this domain. Provider-side retention and billing remain subject to the selected service's terms; cancellation is not a deletion or refund guarantee.

Conformance Cases

These cases define observable checks, not claims that an implementation has passed. Tests use a controllable provider fixture and isolated authenticated endpoints.

ID Exercise Required observation
LLM-C01 Select, describe, generate; also omit the optional domain Matching correlation/results; no handshake dependency; absent-domain caller fallback
LLM-C02 Malformed record, base64, enum or explicit null invalid-request; no provider generation
LLM-C03 Unsupported images/JSON/tools/exact model or provider-specific input Explicit unsupported; no silent downgrade or provider generation
LLM-C04 Decline consent after selecting a valid model user-cancelled; zero provider generations
LLM-C05 Two endpoints reuse IDs and attempt each other's handles/cancel Handles rejected; foreign cancellation false; no cross-delivery
LLM-C06 Duplicate an active generation ID No second provider generation; explicit failure
LLM-C07 Cancel or time out during revalidation, consent or a stalled provider Prompt settlement; no later submission from pending consent; late output suppressed
LLM-C08 Switch account, revoke endpoint or change connection during consent Cancel affected work; invalidate selections; no delivery to the new account
LLM-C09 Buffered and streamed responses; cancellation/completion race Zero-based ordered deltas if any; one terminal result; none after termination
LLM-C10 Limit/refusal, missing finish marker or invalid JSON-object completion Partial results remain partial; malformed completion is provider-error
LLM-C11 Provider failure containing input or credentials Safe typed error; no body, credential or other endpoint data leakage
LLM-C12 Assistant turn with multiple toolCall parts; next request omits one result Request rejected as invalid-request; no provider generation
LLM-C13 toolChoice forcing a named tool; provider ignores it provider-error; no silent substitution or plain-text fallback
LLM-C14 Provider returns no tool call id Runtime synthesizes a stable id; matching toolResult round-trips
LLM-C15 Completed calls with different normal provider stop labels; also truncate/refuse a parallel turn after one call completes Completed calls normalize to toolUse; truncation/refusal preserves finish, no executable calls
LLM-C16 Completed arguments malformed or not an object; unknown tool name provider-error; no raw provider body echoed
LLM-C17 Replay thinking; connection requires opaque reasoning state Thinking omitted from provider input; required unrepresentable state produces unsupported, no generation
LLM-C18 Stream interleaved tool-call blocks Start carries id/name; ordered lifecycle; fragments parse to final arguments; no execution from provisional blocks
LLM-C19 Request an unlisted tool-choice mode unsupported; no substitution or provider generation
LLM-C20 Duplicate/invalid tool names, missing tools for explicit choice, unknown forced name, non-object schema, zero maxTokens invalid-request; no generation
LLM-C21 Misplaced/duplicate/orphaned results; history without current tools Invalid histories rejected; completed history without tools accepted if connection supports it; no new calls
LLM-C22 Image tool result; error result with no provider-native flag Unsupported image placement rejected before submission; error flag mapped to specified text prefix
LLM-C23 Consent declined with sensitive tool definitions/results Disclosure identifies both; no provider submission

Implementations

Changelog

  • 4bb12f3 - Define user-selected inference, endpoint-scoped selections, disclosure consent, generation/cancellation lifecycles, typed errors and conformance cases; add the draft registry entry and reference links.
  • 8f74132 - Widen the wire subset to the common denominator of chat-completions and messages APIs: generation controls (maxTokens, temperature, samplingParams), tool definitions and choice, thinking and toolCall parts, the toolResult role with an error flag, block-aware deltas, cache and reasoning token counts without cost, and Generation.content as a part list; answer parallel-tool-call and provider-synthesized-id questions inline.
  • 8c354ce - Define portable tool names and schemas, supported choice modes, adjacent result placement and error mapping; remove the open sampling bag, require positive maxTokens, make thinking display-only with explicit continuation failure, define block lifecycle events and argument validation, preserve truncation/refusal, and extend consent, security and conformance coverage.