ScoutCode API
ScoutCode speaks the OpenAI Chat Completions API. Anything that works with OpenAI works here: change the base URL and the key. Behind that one endpoint, each request is routed across free models from several providers, and falls back to the next model when one is rate limited, down or slow.
| Base URL | http://localhost:4321/v1 |
|---|---|
| Auth | Authorization: Bearer sk-scout-… |
| Format | JSON in, JSON or server-sent events out |
Quick start
You need a ScoutCode API key. Create an account, then make keys on your account page; they start with sk-scout-. New accounts have no request allowance until the operator raises it: until then every request gets a 429 rate-limit error.
curl
curl http://localhost:4321/v1/chat/completions \
-H "Authorization: Bearer $SCOUTCODE_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "auto",
"messages": [{ "role": "user", "content": "Hello!" }]
}'Python (openai)
import os
from openai import OpenAI
client = OpenAI(base_url="http://localhost:4321/v1", api_key=os.environ["SCOUTCODE_KEY"])
res = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Hello!"}],
)
print(res.choices[0].message.content)JavaScript (openai)
import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'http://localhost:4321/v1', apiKey: process.env.SCOUTCODE_KEY });
const res = await client.chat.completions.create({
model: 'auto',
messages: [{ role: 'user', content: 'Hello!' }],
});
console.log(res.choices[0].message.content);Authentication
Send your key on every request, in either header:
Authorization: Bearer sk-scout-… x-api-key: sk-scout-…
A missing, wrong or disabled key gets 401. Keys are personal: each has its own rate limit, and the operator can disable one without affecting anyone else. Keep yours out of public repositories and client-side code you ship to others.
The API allows cross-origin requests (Access-Control-Allow-Origin: *), so it can be called from a browser.
Choosing a model
The model field accepts three kinds of value:
| Value | What happens |
|---|---|
auto | Uses the server's route: an ordered list of presets, tried top to bottom until one answers. scoutcode,scout, default and an empty string mean the same. |
| A preset id or name | For example gpt-oss-120b or "GPT-OSS 120B". Matching ignores case, spaces and punctuation. All of that preset's provider models are tried first. If they all fail, the request usually continues on theauto route (the operator can turn this off). |
| Anything else | Unknown names, such as a tool's default gpt-4o, are treated as auto rather than rejected. |
Call GET /models to see which presets are available to you. A server can also be set to "one preset only", in which case every request uses that preset whatever model says.
Requests that include tools skip models known not to support tool calling. Requests that include images prefer models known to support vision.
POST /chat/completions
Creates a chat completion. The body is the standard OpenAI request:
| Field | Type | Notes |
|---|---|---|
messages | array, required | Must be non-empty. Roles: system, developer, user, assistant, tool. |
model | string | See Choosing a model. Defaults to auto. |
stream | boolean | Server-sent events. See Streaming. |
tools, tool_choice | array, string or object | See Tool calling. |
max_tokens / max_completion_tokens | integer | Either works; converted to what each provider expects. |
temperature, top_p, stop, seed, response_format | Passed through to the provider. |
Because a request can land on different providers, ScoutCode adjusts it for each one. developer messages become system where needed, and tool-call ids are rewritten for providers with strict id formats, so a conversation can move between providers mid-session. Fields a provider doesn't support are dropped rather than causing an error: logprobs, top_logprobs, parallel_tool_calls,frequency_penalty, presence_penalty, reasoning_effort, stream_optionsand n, depending on the provider.
Response
The standard OpenAI chat.completion object, passed through from the provider that answered. Itsmodel field is that provider's model id. To see which preset served you, read theresponse headers.
{
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1793000000,
"model": "openai/gpt-oss-120b",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hi! How can I help?" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 12, "completion_tokens": 8, "total_tokens": 20 }
}Streaming
With "stream": true the response is text/event-stream: one data: line per chunk in the OpenAI chat.completion.chunk format, ending with data: [DONE].
const stream = await client.chat.completions.create({
model: 'auto',
stream: true,
messages: [{ role: 'user', content: 'Write a haiku about routers.' }],
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}data: {"choices":[{"index":0,"delta":{"role":"assistant","content":"Packets"}}]}
data: {"choices":[{"index":0,"delta":{"content":" drift"}}]}
data: [DONE]ScoutCode waits for the first chunk before it commits to a model, so a stream that fails before its first token still falls back to the next model and you never see the failure. Once tokens are flowing it can't switch models: if the provider drops the connection mid-answer, the stream simply ends. Check that you received [DONE] or afinish_reason if completeness matters.
Tool calling
Send tools in the OpenAI function format. When the model calls a tool, the response hasfinish_reason: "tool_calls" and message.tool_calls. Run the tool, then send its result back as a tool message with the matching tool_call_id.
{
"model": "auto",
"messages": [{ "role": "user", "content": "What's the weather in Paris?" }],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}
]
}Not every free model is equally good at tool calling. Naming a strong preset usually beats auto for agent workloads.
Images
Use an array content with image_url parts. Data URLs (data:image/png;base64,…) are the safe choice; whether plain https URLs work depends on the provider that answers. Requests with images are sent to vision models first.
{
"model": "auto",
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "What is in this picture?" },
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgo…" } }
]
}
]
}GET /models
Lists auto plus every enabled preset. Use the id as model. Raw provider model ids are not listed. This call doesn't count toward your rate limit.
{
"object": "list",
"data": [
{ "id": "auto", "object": "model", "created": 1793000000, "owned_by": "scoutcode", "name": "Auto (your route)" },
{ "id": "gpt-oss-120b", "object": "model", "created": 1793000000, "owned_by": "scoutcode", "name": "GPT-OSS 120B" }
]
}POST /jev/systemone
Your ScoutCode key also works for Jev, TypeSafe's decision model (the one behindspeed-router). Send a state and typed questions (choice,score, noul, box) and get structured answers with probabilities and confidence. The request and response are TypeSafe's own System One format, passed through unchanged, including itsTypeSafe-Danger-Zone-* headers and gzip or zstd bodies.
curl http://localhost:4321/v1/jev/systemone \
-H "Authorization: Bearer $SCOUTCODE_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "jev-latest",
"state": "I was charged twice this month. Please refund the extra charge.",
"questions": {
"team": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": { "billing": "Payments", "technical": "Bugs", "sales": "Pricing" }
},
"urgent": { "type": "noul", "instructions": "The message is urgent" }
}
}'{
"model": "jev-latest",
"answers": {
"team": { "type": "choice", "choice": "billing", "confidence": 0.9, "probabilities": { "billing": 0.95, "technical": 0.03, "sales": 0.02 } },
"urgent": { "type": "noul", "noul": 0.31 }
},
"usage": { "input_tokens": 140, "output_tokens": 14 }
}GET /jev/models lists the Jev models you can name (it doesn't count toward your rate limit). The TypeSafe SDKs work too: point their base URL at http://localhost:4321/v1/jev.
import { choice, noul, TypeSafeClient } from '@typesafe-ai/sdk';
const client = new TypeSafeClient({ baseURL: 'http://localhost:4321/v1/jev', apiKey: process.env.SCOUTCODE_KEY, defaultModel: 'jev-latest' });
const res = await client.systemOne({
state: ticket,
questions: { team: choice('Which team should handle this?', { billing: 'Payments', technical: 'Bugs' }), urgent: noul('The message is urgent') },
});Each POST /jev/systemone counts as one request against your frontier allowance, since Jev is billed per token. TypeSafe's own errors (422 for a malformed question, 429, 529) are passed through as they are.
Response headers
| Header | Meaning |
|---|---|
x-scoutcode-preset | Preset that answered, e.g. gpt-oss-120b |
x-scoutcode-provider | Provider that answered, e.g. groq |
x-scoutcode-model | Provider and model, e.g. groq/openai/gpt-oss-120b |
x-scoutcode-attempts | How many upstream attempts it took, including the one that worked |
x-ratelimit-limit-requests, x-ratelimit-remaining-requests | Your key's per-minute limit and what's left. Sent with 429 responses. |
x-ratelimit-limit-requests-day, x-ratelimit-remaining-requests-day | The same for the daily limit |
Retry-After | Seconds to wait, on 429 responses |
All of these are exposed to browsers through CORS.
Rate limits
Each account has a requests-per-minute limit and a requests-per-day limit, set by the operator and shared by all its keys. You can see them, and today's count, on your account page. Every validPOST /chat/completions counts, whether or not a model manages to answer it. Requests rejected with400, 401 or 413 don't count, and neither does GET /models. The minute window slides; the day resets at 00:00 UTC.
Some accounts have two separate allowances:
| Allowance | Used by |
|---|---|
| Open-source | auto, and presets that run on open-weight models (e.g. gpt-oss-120b) |
| Frontier | Presets that only run on paid frontier models. When an auto request is routed to a frontier model, it also takes one from this allowance; once that is used up, auto sticks to open-source models instead. |
Over a limit you get 429 with a Retry-After header:
{
"error": {
"message": "Rate limit reached for this ScoutCode account (frontier models): 20 requests per minute. Try again in 41s.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}There is a second, separate kind of 429: when every model on your route is rate limited by its provider at the same moment. That one has type: "scoutcode_error" and also carries Retry-After. The official OpenAI SDKs retry both automatically (twice by default). To handle it yourself:
import time, openai
for attempt in range(5):
try:
res = client.chat.completions.create(model="auto", messages=messages)
break
except openai.RateLimitError as e:
wait = int(e.response.headers.get("retry-after", "5"))
time.sleep(wait)Errors
Errors use the OpenAI shape, an error object with a message:
| Status | When | Retry? |
|---|---|---|
400 | Body isn't a JSON object or has no messages | No, fix the request |
413 | Body is over 25 MB (usually too many or too large base64 images) | No, shrink the request |
401 | Missing, invalid or disabled API key | No |
429 rate_limit_error | Your account is over its limit (or has no allowance yet) | After Retry-After |
429 scoutcode_error | Every candidate model is rate limited upstream | After Retry-After |
4xx scoutcode_error | Every model rejected the request (e.g. context too long, invalid tool schema). Carries the provider's status and its explanation | No, change the request |
502 | Every attempt failed with server errors or timeouts | Yes, with backoff |
503 | The server has no usable models configured | Contact the operator |
Routing errors (type: "scoutcode_error") also say how many upstream attempts were made. When reporting a problem, include the time of the request; the operator can see each attempt in the server's log.
{
"error": {
"message": "Every candidate model is rate limited right now.",
"type": "scoutcode_error",
"code": 429,
"attempts": 4
}
}Timeouts and sizes
- Request bodies are limited to 25 MB, including base64 images. Larger requests get
413. - Streaming: each model gets 30 seconds to send its first token before ScoutCode moves on. After that there's no time limit.
- Non-streaming: each model gets up to 3 minutes for the whole answer.
- A request makes at most 10 upstream attempts before giving up.
These are defaults; the operator can change them. For long answers, prefer streaming.
Coding tools
Any tool with an "OpenAI-compatible" provider option works. Use auto or a preset id as the model.
Cline / Roo Code / Kilo Code
API Provider: OpenAI Compatible Base URL: http://localhost:4321/v1 API Key: <your key> Model ID: auto
Continue (config.yaml)
models:
- name: ScoutCode
provider: openai
model: auto
apiBase: http://localhost:4321/v1
apiKey: <your key>Aider
aider --openai-api-base http://localhost:4321/v1 \
--openai-api-key <your key> \
--model openai/autoEnvironment variables
OPENAI_BASE_URL=http://localhost:4321/v1 OPENAI_API_KEY=<your key>