---
title: "Stream a chat completion with an intern"
method: POST
path: "/interns/{internId}/chat/completions"
tags: ["Interns"]
---

# Stream a chat completion with an intern

`POST /interns/{internId}/chat/completions`

Sends a prompt to one of your interns and streams the reply as OpenAI-compatible server-sent events ending with `[DONE]`. The run executes on the intern, which may pause to ask you something. It then streams one `openrouter.provide_input` tool call and finishes with `finish_reason: "tool_calls"`, and the run stays open on the intern.

Every response, whether it ends with `stop`, `tool_calls` or `error`, is followed by a final chunk with empty `choices` that carries `session_id`, then `data: [DONE]`. That chunk carries the `usage` the intern reported for the run, after `stop` or `error`, and `null` when the intern reported none. After `tool_calls` its `usage` is `null` because the turn is not over. Read through `[DONE]`: the `session_id` you need to reply arrives after the `tool_calls` finish chunk.

To answer, send a second request with the same `session_id`, the assistant message echoing that tool call, and a `tool` message whose `tool_call_id` is the tool call id and whose `content` is the answer. The answer is delivered to the run that asked and the stream continues from where it paused. A question stays open for its interaction deadline (5 minutes by default) and the run is cancelled when that passes. Rejected replies do not extend the deadline.

Closing the connection after the `[DONE]` that follows `finish_reason: "tool_calls"` keeps the run alive. Disconnecting while a response is still streaming cancels the run. The stream writes a `: keepalive` comment whenever nothing else has been written for 30 seconds, so a disconnect is noticed within that interval even while the intern is silent.

A run the intern ends while you are still connected, by cancellation or by a deadline, ends the stream with a `finish_reason: "error"` chunk carrying `410` and reason `run_ended`, then the final empty-`choices` chunk and `[DONE]`. That error reports only an ending the intern confirmed. A connection that breaks without that confirmation ends with reason `stream_severed`, and a client that has already disconnected is promised no final event.

Set `approval_mode` to `manual` to have the intern ask before approval-bearing tools such as the shell. Omitted, the run self-drives and consents on your behalf. The mode belongs to the run started by that prompt and must be repeated on later prompts.

Available to interns programme members. Callers outside the programme receive `404` for every path under `/api/v1/interns`.

## Path parameters

- `internId` string, required — The intern to talk to.

## Request body

- InternChatCompletionRequest — An OpenAI-compatible streaming chat completion request for one intern. Other OpenAI fields such as `temperature` or `tools` are ignored. The intern owns its sampling and its tools.
  - `approval_mode` 'manual' | 'self-drive' — How the run started by this prompt handles tool approvals. `self-drive` (the default when omitted) consents on your behalf and runs the shell unsandboxed. `manual` asks you before an approval-bearing tool runs, as an `openrouter.provide_input` permission request, and keeps the shell sandboxed until an escalation is allowed. The mode applies to the run this prompt starts and is not remembered by the session. Repeat it on each new prompt that should use it. A `tool` reply continues the run under the mode it started with.
  - `messages` InternChatMessage[], required — The conversation. Only the last message is read. A last `user` message starts a run. A last `tool` message answers the interaction named by its `tool_call_id` and requires `session_id`.
    - union — One OpenAI-compatible chat message, discriminated by `role`.
      - InternChatSystemMessage — A system message. Accepted for client compatibility and not forwarded.
        - `content` union, required — Message text as a string or a list of text parts. Assistant history may carry null. Only the last message is read; earlier messages are accepted so ordinary clients can resend history.
          - string
          - InternChatTextPart[]
            - `text` string, required
            - `type` 'text', required
        - `role` 'system', required
      - InternChatDeveloperMessage — A developer message. Accepted for client compatibility and not forwarded.
        - `content` union, required — Message text as a string or a list of text parts. Assistant history may carry null. Only the last message is read; earlier messages are accepted so ordinary clients can resend history.
          - string
          - InternChatTextPart[]
            - `text` string, required
            - `type` 'text', required
        - `role` 'developer', required
      - InternChatUserMessage — A user message. When it is the last message its text is the prompt for a new run, at most 32000 characters.
        - `content` union, required — Message text as a string or a list of text parts. Assistant history may carry null. Only the last message is read; earlier messages are accepted so ordinary clients can resend history.
          - string
          - InternChatTextPart[]
            - `text` string, required
            - `type` 'text', required
        - `role` 'user', required
      - InternChatAssistantMessage — An assistant message from an earlier response. When answering an interaction, echo the streamed `tool_calls` here before the `tool` message.
        - `content` union — Message text as a string or a list of text parts. Assistant history may carry null. Only the last message is read; earlier messages are accepted so ordinary clients can resend history.
          - string
          - InternChatTextPart[]
            - `text` string, required
            - `type` 'text', required
        - `role` 'assistant', required
        - `tool_calls` InternChatEchoedToolCall[]
          - `function` InternChatEchoedToolCallFunction, required — The function name and JSON arguments string exactly as streamed.
            - `arguments` string, required
            - `name` string, required
          - `id` string, required
      - InternChatToolMessage — The answer to an `openrouter.provide_input` tool call. `tool_call_id` is the streamed tool call id and `session_id` must name the same session. For a permission, `content` is one of the offered option kinds (`allow_once`, `allow_always`, `reject_once`, `reject_always`) or `cancel`. For a question (elicitation), `content` is a JSON object string with `action` (`accept`, `decline` or `cancel`) and, for `accept`, `content` holding the field values. The answer is delivered to the run that asked. It never starts a new run.
        - `content` union, required — Message text as a string or a list of text parts. Assistant history may carry null. Only the last message is read; earlier messages are accepted so ordinary clients can resend history.
          - string
          - InternChatTextPart[]
            - `text` string, required
            - `type` 'text', required
        - `role` 'tool', required
        - `tool_call_id` string, required
  - `model` string — Accepted for OpenAI compatibility and never used. The intern runs the model configured on it (`PATCH` the intern to change it). Streamed chunks report the runtime's identifier for that model as the intern reports it, or `openrouter/intern` on chunks whose event carries no model (before the intern reports one, and on the chunks the API emits itself: the timeout, run-ended and severed-stream error chunks, the stop chunk of a replay that ends without a terminal daemon event, and the final usage chunk after any of them). A usage chunk that follows a daemon completion event carries the model the intern reported.
  - `session_id` string — The daemon session to continue, as returned in `session_id` on the final chunk of an earlier response. Omit it to start a new session. An id the intern has not seen before is not an error: it starts a new session under that id, so a mistyped id forks the conversation. Sessions are scoped to the intern's own daemon. Required when the last message has role `tool`.
  - `stream` true, required — Must be `true`. This endpoint only streams. `false` or an omitted `stream` is refused with `400` and reason `bad_request`.

## Response `200`

The streamed completion. Chunks carry text, then a finish chunk: `finish_reason: "stop"` when the turn is complete, `finish_reason: "tool_calls"` when the intern is waiting for an answer, or `finish_reason: "error"` for a failure after this status. A final chunk with empty `choices` follows in every case, carrying `session_id` and `usage` (`null` unless the intern reported usage, and always `null` after `tool_calls`), and the stream ends with `data: [DONE]`.

## Other responses

- `400` — The body is not a valid request (`bad_request`), or the intern did not accept the answer for the pending question, such as a permission option it did not offer.
- `401` — No valid API key.
- `403` — The key belongs to a member who has left the organization and holds no grant.
- `404` — The caller is outside the interns programme, the intern does not exist for this key (`not_found`), or no pending question has this `tool_call_id` in this session (`interaction_unknown`).
- `409` — The intern is not running (`intern_not_ready`), another turn is running in this session (`busy`), the question is no longer waiting (`interaction_not_pending`), or another request is already attached to the run (`attachment_failed`).
- `410` — The run produced more output than the intern retains, so the paused stream cannot be resumed (`attachment_failed`).
- `413` — The body exceeds 1 MiB (`payload_too_large`).
- `429` — Too many turns for the user or organization this key acts as (`rate_limited`).
- `502` — The intern could not be reached or rejected the request (`intern_unreachable`, `intern_rejected`), the intern refused the turn before any output (`turn_failed`), or its stream ended before the turn started (`stream_severed`).
- `503` — The intern cannot hold another run open across a question right now (`busy`). Retry later.
- `504` — The intern did not answer within the request budget (`timeout`).

## Changes

- **2026-09-19** `e82559908cfc` — 1 info
  - the request property `messages/items/oneOf[#/components/schemas/InternChatAssistantMessage]/content` became optional
- **2026-09-17** `0620e7774bca` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/openrouterteam/apis/openrouter-api/changes/interns/:internId/chat/completions/post.md)

---

[API](https://skmtc.dev/openrouterteam/apis/openrouter-api.md) · [All operations](https://skmtc.dev/openrouterteam/apis/openrouter-api/llms.txt) · [OpenAPI document](https://skmtc.dev/openrouterteam/apis/openrouter-api/revisions/2b75a8109d8f?raw)
