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 money until the operator gives them some: until then they can use free models at 5/min, 100/day.
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 belongs to an account with its own money, 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 is free.
{
"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's free). 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 answered POST /jev/systemone is free and counts as a free-model request. TypeSafe's own errors (422 for a malformed question, 429, 529) are passed through as they are.
POST /batches
Send up to 5,000 chat requests in one call and collect the answers later, inOpenRouter's batch format. Each request gets acustom_id and a body like a normal /chat/completions request. The top-levelmodel (default auto) is used for every request that doesn't name its own. The call answers202 with the batch, and ScoutCode works through it in the background.
curl http://localhost:4321/v1/batches \
-H "Authorization: Bearer $SCOUTCODE_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "auto",
"requests": [
{ "custom_id": "review-1", "body": { "messages": [{ "role": "user", "content": "Classify: The fit is perfect." }] } },
{ "custom_id": "review-2", "body": { "messages": [{ "role": "user", "content": "Classify: Arrived broken." }] } }
]
}'Poll GET /batches/{id} until status iscompleted. results fills in as answers arrive, in the order they finish, so match them bycustom_id. A request that failed has an error (code, message) instead of a response.
{
"id": "batch_3f9c…",
"object": "batch",
"endpoint": "/v1/chat/completions",
"model": "auto",
"status": "completed",
"created_at": 1793145600,
"completed_at": 1793145642,
"expires_at": 1793232000,
"request_counts": { "total": 2, "completed": 2, "failed": 0 },
"usage": { "cost": 0.002, "prompt_tokens": 38, "completion_tokens": 6 },
"results": [
{ "custom_id": "review-2", "response": { "status_code": 200, "body": { "choices": [{ "message": { "role": "assistant", "content": "negative" } }], … } } },
{ "custom_id": "review-1", "response": { "status_code": 200, "body": { … } } }
]
}- Each request is routed and charged exactly like a normal one, at the price of the preset that answered.
usage.costis what the batch has cost so far. - A request that is rate limited (including your own free-model limits) or fails upstream waits and is tried again, so a batch can finish on free models without hitting
429s. Requests the models reject (4xx) or that your account can't pay for (402) fail right away. - A batch has 24 hours to finish; whatever is left then fails and the batch is
expired. Finished batches and their results are kept for 24 hours. - POST
/batches/{id}/cancelstops a batch (answers already in are kept), DELETE/batches/{id}cancels it and deletes its results, and GET/batcheslists your batches (?limit=,?after=). - Only
/v1/chat/completionsbatches are supported, answers are never streamed, andcallback_urlis ignored: poll instead. An account can run 3 batches at once and keep 50.
Batches are held in the server's memory, never in its database, so a server restart loses every batch, finished or not. Batch requests run a few at a time, sharing the same models as everyone's live requests.
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-scoutcode-cost | What this request cost you, in USD |
x-scoutcode-balance | What your account has left to spend after it, in USD (today's money plus your balance) |
x-scoutcode-free-tier | On free-model answers: which free limits apply, starter or paying |
x-scoutcode-free-left-minute | On free-model answers: free requests left in the current minute |
x-scoutcode-free-left-today | On free-model answers: free requests left today (UTC) |
Retry-After | Seconds to wait, on 429 responses; on 402, seconds until your money per day comes back |
All of these are exposed to browsers through CORS.
Money and pricing
Each model has a price, shown on the Models page and your account page: a set amount per request, an amount per million input and output tokens, or both. Your account pays for requests from two places, shared by all its keys:
| Money | How it works |
|---|---|
| Money per day | An amount the operator gives your account each UTC day. Whatever is left at 00:00 UTC is gone, and you get a fresh amount. |
| Balance | Money the operator adds to your account. It doesn't reset, and is only used once today's money is spent. |
You pay only for answered requests, at the price of the model that answered: with auto or the speed router that is whichever preset ended up answering. Token prices use the token counts the provider reports (ScoutCode asks for them on streams, so a final usage chunk arrives even if you didn't set stream_options), or an estimate of about 4 characters a token when it reports none. A streamed answer you cancel is charged for what was sent. Failed requests are free, as are GET /models and requests rejected with 400, 401 or 413. Models your account can't afford are skipped, soauto falls back to cheaper ones. When it can't afford any, you get 402:
{
"error": {
"message": "Not enough money on this ScoutCode account: GPT 6.1 Sol needs at least $0.0004 for this request and $0.0001 is left. Its money per day comes back at 00:00 UTC, or ask the ScoutCode admin to add more.",
"type": "insufficient_quota",
"code": "insufficient_quota"
}
}Answered requests carry x-scoutcode-cost and x-scoutcode-balance, so you can watch your spending without opening the account page. Streams from token-priced models don't, since they're charged after the headers are sent. Some accounts are unlimited and never pay.
Free models
Free models cost nothing. Instead, each account has a limit on how many requests they answer, shared by all its keys:
| Account | Free-model requests |
|---|---|
| No money per day and an empty balance | 5/min, 100/day |
| Money per day, or a balance | 15/min, 2,000/day |
The daily count resets at 00:00 UTC. Once you reach a limit, free models are skipped: auto moves on to paid models if your account can pay for them, and otherwise you get a 429 with code: "rate_limit_exceeded"and a Retry-After saying when you can send the next one. Unlimited accounts have no free-model limits.
A 429 with type: "scoutcode_error" is something else: every model on your route is rate limited by its provider at the same moment. It has type: "scoutcode_error" and carries Retry-After. The official OpenAI SDKs retry it 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 |
402 insufficient_quota | Your account doesn't have enough money for any model on the route (e.g. you named a paid model and have no money yet) | After your money per day comes back, or once the operator adds more |
429 rate_limit_exceeded | Your account used its free-model requests for now and can't pay for a paid model | 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>