POST /api/v1/interns/{internId}/chat/completions streams a turn from one of your interns in the OpenAI chat completions format. Your prompt runs on the intern with its own tools and context. When the intern needs something from you, a permission to run a command or an answer to a question, the stream pauses as a tool call, and you answer it with a second request that continues the same run.
This endpoint is available to members of the interns programme. A key outside the programme receives 404 for every path under /api/v1/interns. Provisioning keys are refused with 401. The full request and response schema is in the API reference.
An ordinary turn
Sendstream: true and a messages array. Streaming is required, and the request is refused with 400 without it.
text/event-stream. Assistant text arrives as delta.content chunks, then a chunk with finish_reason: "stop", then a final chunk with empty choices that carries usage and session_id, then data: [DONE].
session_id. Sending it with a later prompt continues the same intern session, so the intern remembers the earlier turns. Omit it to start a fresh session. Heartbeats arrive as SSE comment lines (: ...) while the intern works, and your client should ignore them.
When the intern needs you
Some turns cannot finish without you. The intern may ask a question, or in manual approval mode it may need permission to run a command. Rather than waiting on an open stream, the endpoint pauses the run and ends the first response with one tool call namedopenrouter.provide_input and finish_reason: "tool_calls". Read through data: [DONE]: the final chunk with empty choices arrives after that finish chunk and carries the session_id you need to reply, with usage: null because the turn is not over.
[DONE] is expected and does not cancel anything. The run stays paused on the intern, waiting for your answer, until the interaction deadline described below.
The tool call’s arguments is a JSON string with a kind and the fields for that kind.
Permission (kind: "permission"). operation describes what the intern wants to do. options lists the answers the intern accepts for this request, drawn from allow_once, allow_always, reject_once and reject_always. Only the listed options are accepted.
Question (kind: "elicitation"). message is the question. fields lists what to answer, each with a name, a type, whether it is required, and optionally the options the intern suggests.
Answering in a second request
Send a new request with the samesession_id, the conversation so far, the assistant message that carried the tool call, and a tool message whose tool_call_id matches the tool call’s id. The content of the tool message is your answer. Send only the stream, session_id and messages fields. The reply continues the run in the approval mode it started with, so approval_mode is ignored here.
finish_reason: "stop", usage and the same session_id. If the intern needs you again, it pauses again with a new tool call and you repeat this step. The reply is delivered to the paused run itself. It does not start a new turn and it does not replay your prompt.
The tool message content depends on the kind of request.
Permission. One of the offered options as a plain string, for example "allow_once" or "reject_once". To abandon the operation, send "cancel". After reject_once or cancel the intern continues without running that operation and reports what it did instead.
Question. A JSON string with an action. To answer, send {"action":"accept","content":{...}} with one property per field name. To decline the question, send {"action":"decline"}. To cancel it, send {"action":"cancel"}. The intern continues either way, without an answer when you decline or cancel.
session_id, and the tool_call_id must belong to a paused run in that session. An answer the intern does not accept, such as a permission option it did not offer or a question answer sent to a permission request, is refused with 400 and the run stays paused for a corrected reply.
Approval mode
approval_mode is an optional request field with two values.
self-drive is the default when the field is omitted. The intern consents to its own tool use, so it does not pause for permission. It still pauses for questions.
manual makes the intern ask you before it uses a tool that needs approval, through the permission tool call above. It also changes the shell tool. Commands run inside a sandbox until you allow the escalation the intern asks for.
session_id runs in self-drive unless it repeats approval_mode: "manual". Tool replies always continue the run in the mode it started with. Any other value is refused with 400.
Deadlines and disconnects
A paused run waits five minutes for your answer. The interaction deadline is measured from the moment the intern asked, and a rejected reply does not extend it. When it passes, the intern ends the run, and a reply after that is refused with409 and reason interaction_not_pending.
Closing an active stream cancels the run. The only close that preserves the run is the one after the data: [DONE] that follows finish_reason: "tool_calls". If your client disconnects while the intern is still working, either on the first request or on the request that carried your answer, the run is cancelled, including any command it is running. Detection is not immediate. While the intern is silent, the disconnect is noticed on a later heartbeat, and that may take more than one heartbeat interval. Do not rely on cancellation timing for anything the intern must not do.
A cancellation you are still connected for is reported. When the intern ends a run you are streaming, by cancellation or by a deadline, the stream ends with a finish_reason: "error" chunk carrying 410 and reason run_ended, then the final empty-choices chunk and data: [DONE]. That error is sent only for an ending the intern confirmed. A connection that simply breaks carries no such confirmation and ends with reason stream_severed, so the two are never confused. A client that has already disconnected is promised no final event at all.
Each request has its own budget. A single request streams for at most five minutes. A turn that outlives its request ends with a streamed 504 error and reason timeout.
Errors
Errors arrive in two ways, depending on whether the response headers were already sent. Refused requests return a JSON body with a non-2xx status. Theerror.metadata.reason field is stable and is the value to branch on. The message is safe to show to a person.
Responses from the shared authentication and membership checks (
401, 403, and the programme 404) carry error.code and error.message without metadata.
Failures after the stream started arrive as a chunk with finish_reason: "error" and an error object in the same shape, followed by the final empty-choices chunk and data: [DONE]. The reasons are attachment_failed, busy, client_closed_request, interaction_not_pending, interaction_unknown, intern_unreachable, run_ended, stream_severed, timeout and turn_failed.
session_id if you want the intern to keep the session’s context.