Chat Completions API
NSED exposes a standard OpenAI-compatible /v1/chat/completions endpoint that
makes it plug-and-play with Cursor, Chatbox, and any Vercel AI SDK wrapper.
Endpoint
POST /v1/chat/completions
Authorization: Bearer <token>
Content-Type: application/json
Request Format
Standard OpenAI Chat Completions schema:
{
"model": "nsed:*",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is 2 + 2?"}
],
"stream": true,
"max_tokens": 1000,
"tools": [
{
"type": "function",
"function": {
"name": "calculator",
"description": "Evaluate math expressions",
"parameters": {"type": "object", "properties": {}}
}
}
]
}
Model Resolution
The model field maps to NSED policies:
All model strings are resolved by resolve_policy() via tag lookup
against the policy registry. The execution mode depends on the resolved
policy's mode field, not the model name itself. The examples below
reflect the default simulation config — production deployments may
define different policies for the same tags.
| Model string | Resolution | Default simulation behaviour |
|---|---|---|
nsed:fast |
Tag lookup → first policy tagged "fast" |
1-round deliberation |
nsed:deep |
Tag lookup → first policy tagged "deep" |
5-round deliberation |
nsed:moderated |
Tag lookup → first policy tagged "moderated" |
Single-agent moderator |
nsed:passthrough |
Tag lookup → first policy tagged "passthrough" |
Single-agent passthrough |
nsed:* |
Wildcard → first registered policy | depends on registry |
nsed:<tag> |
Tag lookup → first matching policy | depends on registry |
nsed:<policy_id> |
Exact content-hash match | depends on policy |
Execution Modes
The resolved policy's mode field controls which execution path the
handler takes:
| Mode | Path | Use for |
|---|---|---|
Deliberation (default) |
Full propose → evaluate → converge cycle via JetStream | Code review, architecture decisions, multi-agent deliberation |
Passthrough |
Direct NATS request-reply to a single agent | Autocomplete, linting, raw utility tasks |
Moderator |
Direct NATS request-reply to the moderator: true role agent |
Title generation, summaries, lightweight admin tasks |
Both Passthrough and Moderator bypass JetStream orchestration
entirely: no room state, no KV buckets, no deliberation rounds. The
response is synchronous — the agent receives the request, calls its
LLM, and replies. When stream: true, the handler wraps the
synchronous response as a minimal SSE sequence (role chunk → content
chunk → finish chunk → [DONE]).
NSED Metadata
Responses from Passthrough and Moderator modes include an optional
nsed_metadata field:
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"choices": [{ "message": { "content": "..." } }],
"nsed_metadata": {
"mode": "moderator"
}
}
Clients that don't understand nsed_metadata ignore it (OpenAI clients
tolerate extra fields). Clients that DO understand it (e.g. OpenCode)
can use it to skip their own title-agent invocation when
mode == "moderator".
Known Limitations
| Limitation | Impact | Workaround |
|---|---|---|
Moderator and passthrough reuse the same NATS passthrough subject |
No explicit opt-in from agents for moderation tasks | Pin moderator role to a specific agent in the policy config |
| Agent worker event loop serializes all message types | A moderator request blocks behind in-flight deliberation tasks on the same agent | Pin the moderator role to a dedicated agent not shared with deliberation policies |
NATS subject prefix hardcoded as "nsed" for passthrough subjects |
Must stay in sync with agent SDK default manually | Documented with TODO in submit_single_agent |
nsed_metadata only on ChatCompletionResponse, not on SSE chunks |
Streaming clients cannot inspect the mode mid-stream | Mode is evident from the model tag (nsed:moderated) |
Response Format
Non-Streaming (stream: false)
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1234567890,
"model": "nsed:fast",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The answer is 4."
},
"finish_reason": "stop"
}
]
}
Streaming (stream: true)
SSE stream of chat.completion.chunk events:
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"reasoning_content":"Round 1 complete — convergence: 45%"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"The answer is 4."},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Reasoning Content
NSED deliberation rounds are emitted as reasoning_content deltas (DeepSeek-R1/O3
convention). Clients that support reasoning display it as thinking blocks; others
silently ignore the field.
Tool Calls
When tools are defined and the deliberation engine triggers a tool call, the
response includes tool_calls in the delta with finish_reason: "tool_calls":
{
"choices": [{
"delta": {
"tool_calls": [{
"index": 0,
"id": "call_abc",
"type": "function",
"function": {"name": "calculator", "arguments": "{\"expr\":\"2+2\"}"}
}]
},
"finish_reason": "tool_calls"
}]
}
Keep-Alive
NSED jobs can be long-running. During idle periods (workers evaluating but not emitting text), the server sends SSE keep-alive comments every 15 seconds:
: keep-alive
This prevents client-side TCP/HTTP timeout disconnects and the infinite retry loops they cause.
Deduplication
The chat completions endpoint uses the same content-hash-based dedup cache as the Responses API. Deduplication is handled by the orchestrator, not this SDK.
Session Continuation & Mid-Flight Injection
Clients that want to pin a multi-turn chat to a stable thread on the server
can send an x-opencode-session header (the same one OpenCode uses). NSED
uses that header to derive a stable thread base and routes requests
according to this rule:
| State of prior job under this session | stream: true |
stream: false |
|---|---|---|
| No prior job | Submit new job with room_id = {thread_base}-{timestamp_ms}; return SSE chunks |
Submit new job; block until completion and return a chat.completion JSON response |
Prior job Running |
Inject the latest user message; reconnect the caller to the existing SSE event stream | Inject the latest user message; block until the prior job reaches a terminal state and return a chat.completion JSON response |
Prior job Completed / Failed |
Submit new job with room_id = {thread_base}-{timestamp_ms}; return SSE chunks |
Submit new job; block until completion and return a chat.completion JSON response |
In every fresh-job case the full conversation history is already present
in the request's messages[] array (clients echo it on every turn), so
context is preserved even when we cannot inject into a live deliberation.
Each turn gets a unique {thread_base}-{timestamp_ms} room_id so that a
second turn arriving after the first has completed cannot collide with the
terminal KV entry of the prior job. Mid-deliberation follow-ups (message 2
sent while message 1 is still streaming) land as user injections on the
currently-active round rather than spinning up a parallel job.
Note: Mid-flight injection requires the client to actually send a second request while the first is still streaming. Standard OpenAI chat clients (including OpenCode) block user input until the current SSE stream completes, so the injection path mostly fires when a separate tool or script posts a follow-up via a parallel HTTP call.
Example — mid-flight injection
# Terminal 1: start a slow deliberation
curl -N -X POST http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $NSED_TOKEN" \
-H "x-opencode-session: ses_example" \
-d '{
"model": "nsed:deep",
"messages": [{"role": "user", "content": "Design a distributed cache"}],
"stream": true
}'
# Terminal 2 (a few seconds later, while ^ is still streaming):
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $NSED_TOKEN" \
-H "x-opencode-session: ses_example" \
-d '{
"model": "nsed:deep",
"messages": [
{"role": "user", "content": "Design a distributed cache"},
{"role": "user", "content": "Focus on memory safety over throughput"}
],
"stream": false
}'
The server scans for an active job under oc-ses_example, finds the
deliberation from terminal 1, and injects "Focus on memory safety over throughput". Because terminal 2 set stream: false, the server then
blocks on the injected job until it reaches a terminal state and returns
the final chat.completion JSON response — whereas terminal 1, still
holding its stream: true SSE connection, continues to receive chunks
and sees a user_injection event at the next phase boundary as the
deliberation adapts to the new guidance. Had terminal 2 also used
stream: true, it would have been reconnected to the shared SSE event
stream instead.
NSED-specific SSE delta fields
The chat-completions translator adds side-channel fields to each
chat.completion.chunk that OpenAI-spec clients ignore but NSED-aware
UIs (the /chat SPA, dashboards) consume. These ride on the choices[0].delta
object alongside content / reasoning_content.
| Field | When | Shape | Purpose |
|---|---|---|---|
nsed_session_id |
First chunk of the stream | string |
Session id for inject/finalize/resume endpoints. Also sent as x-nsed-session-id response header. |
nsed_round |
Each proposal_submitted and each round_summary |
{round, total_rounds, progress, scored, candidates: [...]} |
Structured carousel payload — see below |
nsed_injected |
user_injection event from the orchestrator |
{message, round, sequence} |
UI uses this to split the current assistant bubble and render the nudge as a fresh user bubble |
nsed_finalized |
user_finalized event (local user pick) |
{chosen_agent_id, round} |
UI badges the chosen candidate and collapses the carousel |
nsed_round candidate shape
Each entry of candidates[]:
{
"agent_id": "CortexA",
"score": 0.47,
"content_preview": "...full proposal text...",
"thought_preview": "...untruncated reasoning...",
"claims": [
{"claim": "O(n log n) sort", "verdict": "verified", "reason": "..."},
{"claim": "handles empty input", "verdict": "contested", "reason": "..."}
]
}
content_previewcarries the full proposal text (no server-side char cap). UIs should apply a CSSline-clampfor card views and show the full body in detail modals.thought_previewis the fullthought_process— untruncated for the same reason.claims[]collapses duplicate claim assessments across evaluators by worst-verdict-wins (wrong>contested>unverified>verified) so the UI never masks a dissent behind a later approval. Capped at 8.scored: falseonproposal_submitted-derived payloads (no evals yet);scored: truewhen driven by around_summarywith aggregated scores.
/resume endpoint
POST /deliberation/{session_id}/resume reattaches a client to a live
deliberation that the SSE connection lost (network blip, page reload,
mobile sleep). The server replays the full event history from the job's
persisted KV bucket via DeliverPolicy::All, then tails live events,
so the client sees identical content to an uninterrupted stream.
Request body: empty. Returns an SSE stream shaped identically to
/v1/chat/completions responses.
The chat UI auto-invokes /resume on transient disconnects using the
nsed_session_id captured from the first chunk of the original stream.
Architecture
sequenceDiagram
participant Client
participant Handler as /v1/chat/completions
participant Dedup as ResponseCache
participant NATS as NATS Broker
participant Agents as Agent Workers
Client->>Handler: POST (messages, model, stream)
Handler->>Dedup: try_claim(content_hash)
alt Cache Hit
Dedup-->>Handler: Hit(cached)
Handler-->>Client: Cached response
else Owned
Handler->>NATS: submit_deliberation(params)
NATS->>Agents: Dispatch rounds
Agents->>NATS: Events (round_summary, job_complete)
NATS-->>Handler: SSE chunks (chat.completion.chunk)
Handler-->>Client: Stream / JSON response
endClient Configuration Examples
Cursor
{
"openai.apiBase": "http://localhost:8080/v1",
"openai.apiKey": "your-token"
}
Chatbox
Provider: OpenAI-compatible
Base URL: http://localhost:8080/v1
Model: nsed:*
Vercel AI SDK
import { createOpenAI } from '@ai-sdk/openai';
const nsed = createOpenAI({
baseURL: 'http://localhost:8080/v1',
apiKey: 'your-token',
});