curl --request POST \
--url https://openrouter.ai/api/v1/interns/{internId}/chat/completions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"approval_mode": "manual",
"messages": [
{
"content": "Summarize the open pull requests.",
"role": "user"
}
],
"stream": true
}
'import requests
url = "https://openrouter.ai/api/v1/interns/{internId}/chat/completions"
payload = {
"approval_mode": "manual",
"messages": [
{
"content": "Summarize the open pull requests.",
"role": "user"
}
],
"stream": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
approval_mode: 'manual',
messages: [{content: 'Summarize the open pull requests.', role: 'user'}],
stream: true
})
};
fetch('https://openrouter.ai/api/v1/interns/{internId}/chat/completions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://openrouter.ai/api/v1/interns/{internId}/chat/completions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'approval_mode' => 'manual',
'messages' => [
[
'content' => 'Summarize the open pull requests.',
'role' => 'user'
]
],
'stream' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://openrouter.ai/api/v1/interns/{internId}/chat/completions"
payload := strings.NewReader("{\n \"approval_mode\": \"manual\",\n \"messages\": [\n {\n \"content\": \"Summarize the open pull requests.\",\n \"role\": \"user\"\n }\n ],\n \"stream\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://openrouter.ai/api/v1/interns/{internId}/chat/completions")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"approval_mode\": \"manual\",\n \"messages\": [\n {\n \"content\": \"Summarize the open pull requests.\",\n \"role\": \"user\"\n }\n ],\n \"stream\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://openrouter.ai/api/v1/interns/{internId}/chat/completions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"approval_mode\": \"manual\",\n \"messages\": [\n {\n \"content\": \"Summarize the open pull requests.\",\n \"role\": \"user\"\n }\n ],\n \"stream\": true\n}"
response = http.request(request)
puts response.read_body{
"data": {
"choices": [
{
"delta": {
"tool_calls": [
{
"function": {
"arguments": "{\"kind\":\"permission\",\"operation\":\"shell\",\"options\":[\"allow_once\",\"allow_always\",\"reject_once\",\"reject_always\"]}",
"name": "openrouter.provide_input"
},
"id": "15e90ad6-5320-4a59-af4f-b371428154fa",
"index": 0,
"type": "function"
}
]
},
"finish_reason": null,
"index": 0
}
],
"created": 1789537541,
"id": "chatcmpl-f727571a-3bad-4e0d-8a9e-f18f8cda9750",
"model": "openrouter/intern",
"object": "chat.completion.chunk"
}
}{
"error": {
"code": 400,
"message": "The intern did not accept that answer for this tool_call_id.",
"metadata": {
"reason": "bad_request"
}
}
}{
"error": {
"code": 401,
"message": "Invalid or missing API key"
}
}{
"error": {
"code": 403,
"message": "This API key's creator is no longer a member of the organization. Ask an organization admin to reassign the key to an active member."
}
}{
"error": {
"code": 404,
"message": "No pending question has this tool_call_id.",
"metadata": {
"reason": "interaction_unknown"
}
}
}{
"error": {
"code": 409,
"message": "The run that asked this question has already ended.",
"metadata": {
"reason": "interaction_not_pending"
}
}
}{
"error": {
"code": 410,
"message": "The run produced more output than the intern retains for replay.",
"metadata": {
"reason": "attachment_failed"
}
}
}{
"error": {
"code": 413,
"message": "Request body exceeds 1048576 bytes",
"metadata": {
"reason": "payload_too_large"
}
}
}{
"error": {
"code": 429,
"message": "Too many intern turns. Please wait a moment.",
"metadata": {
"reason": "rate_limited"
}
}
}{
"error": {
"code": 502,
"message": "The intern could not be reached.",
"metadata": {
"reason": "intern_unreachable"
}
}
}{
"error": {
"code": 503,
"message": "The intern cannot hold another run open across a question right now. Retry later.",
"metadata": {
"reason": "busy"
}
}
}{
"error": {
"code": 504,
"message": "The intern did not answer in time.",
"metadata": {
"reason": "timeout"
}
}
}Stream a chat completion with an intern
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 disconnect is noticed when the intern next writes to the stream, which during a silent tool run can take more than one 30 second heartbeat interval.
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.
curl --request POST \
--url https://openrouter.ai/api/v1/interns/{internId}/chat/completions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"approval_mode": "manual",
"messages": [
{
"content": "Summarize the open pull requests.",
"role": "user"
}
],
"stream": true
}
'import requests
url = "https://openrouter.ai/api/v1/interns/{internId}/chat/completions"
payload = {
"approval_mode": "manual",
"messages": [
{
"content": "Summarize the open pull requests.",
"role": "user"
}
],
"stream": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
approval_mode: 'manual',
messages: [{content: 'Summarize the open pull requests.', role: 'user'}],
stream: true
})
};
fetch('https://openrouter.ai/api/v1/interns/{internId}/chat/completions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://openrouter.ai/api/v1/interns/{internId}/chat/completions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'approval_mode' => 'manual',
'messages' => [
[
'content' => 'Summarize the open pull requests.',
'role' => 'user'
]
],
'stream' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://openrouter.ai/api/v1/interns/{internId}/chat/completions"
payload := strings.NewReader("{\n \"approval_mode\": \"manual\",\n \"messages\": [\n {\n \"content\": \"Summarize the open pull requests.\",\n \"role\": \"user\"\n }\n ],\n \"stream\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://openrouter.ai/api/v1/interns/{internId}/chat/completions")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"approval_mode\": \"manual\",\n \"messages\": [\n {\n \"content\": \"Summarize the open pull requests.\",\n \"role\": \"user\"\n }\n ],\n \"stream\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://openrouter.ai/api/v1/interns/{internId}/chat/completions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"approval_mode\": \"manual\",\n \"messages\": [\n {\n \"content\": \"Summarize the open pull requests.\",\n \"role\": \"user\"\n }\n ],\n \"stream\": true\n}"
response = http.request(request)
puts response.read_body{
"data": {
"choices": [
{
"delta": {
"tool_calls": [
{
"function": {
"arguments": "{\"kind\":\"permission\",\"operation\":\"shell\",\"options\":[\"allow_once\",\"allow_always\",\"reject_once\",\"reject_always\"]}",
"name": "openrouter.provide_input"
},
"id": "15e90ad6-5320-4a59-af4f-b371428154fa",
"index": 0,
"type": "function"
}
]
},
"finish_reason": null,
"index": 0
}
],
"created": 1789537541,
"id": "chatcmpl-f727571a-3bad-4e0d-8a9e-f18f8cda9750",
"model": "openrouter/intern",
"object": "chat.completion.chunk"
}
}{
"error": {
"code": 400,
"message": "The intern did not accept that answer for this tool_call_id.",
"metadata": {
"reason": "bad_request"
}
}
}{
"error": {
"code": 401,
"message": "Invalid or missing API key"
}
}{
"error": {
"code": 403,
"message": "This API key's creator is no longer a member of the organization. Ask an organization admin to reassign the key to an active member."
}
}{
"error": {
"code": 404,
"message": "No pending question has this tool_call_id.",
"metadata": {
"reason": "interaction_unknown"
}
}
}{
"error": {
"code": 409,
"message": "The run that asked this question has already ended.",
"metadata": {
"reason": "interaction_not_pending"
}
}
}{
"error": {
"code": 410,
"message": "The run produced more output than the intern retains for replay.",
"metadata": {
"reason": "attachment_failed"
}
}
}{
"error": {
"code": 413,
"message": "Request body exceeds 1048576 bytes",
"metadata": {
"reason": "payload_too_large"
}
}
}{
"error": {
"code": 429,
"message": "Too many intern turns. Please wait a moment.",
"metadata": {
"reason": "rate_limited"
}
}
}{
"error": {
"code": 502,
"message": "The intern could not be reached.",
"metadata": {
"reason": "intern_unreachable"
}
}
}{
"error": {
"code": 503,
"message": "The intern cannot hold another run open across a question right now. Retry later.",
"metadata": {
"reason": "busy"
}
}
}{
"error": {
"code": 504,
"message": "The intern did not answer in time.",
"metadata": {
"reason": "timeout"
}
}
}Authorizations
API key as bearer token in Authorization header
Path Parameters
The intern to talk to.
"a11e0000-0000-4000-8000-000000000005"
Body
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.
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.
1One OpenAI-compatible chat message, discriminated by role.
- Option 1
- Option 2
- Option 3
- Option 4
- Option 5
Show child attributes
Show child attributes
{ "content": "Summarize the open pull requests.", "role": "user" }
Must be true. This endpoint only streams.
true
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.
manual, self-drive "manual"
Echoed as model on the streamed chunks; the final chunk may carry the model the intern reported instead. The intern chooses its own model, so this value does not change what runs.
1 - 256"openrouter/intern"
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. Required when the last message has role tool.
1 - 256"ses_7f3c9a"
Response
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].
A server-sent event carrying one chunk. The stream ends with data: [DONE].
One data: line of the stream. A run streams a role chunk, content and reasoning chunks, then a finish chunk: stop, tool_calls (the run is paused for input) or error (with an error object). A final chunk with empty choices follows in every case, carrying session_id and usage (null unless the daemon reported usage, and always null after tool_calls). Every stream ends with [DONE].
Show child attributes
Show child attributes
{ "choices": [ { "delta": { "content": "Hello" }, "finish_reason": null, "index": 0 } ], "created": 1789537541, "id": "chatcmpl-f727571a-3bad-4e0d-8a9e-f18f8cda9750", "model": "openrouter/intern", "object": "chat.completion.chunk" }