ScoutCode
Private preview

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 URLhttp://localhost:4321/v1
AuthAuthorization: Bearer sk-scout-…
FormatJSON 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:

ValueWhat happens
autoUses 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 nameFor 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 elseUnknown 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:

FieldTypeNotes
messagesarray, requiredMust be non-empty. Roles: system, developer, user, assistant, tool.
modelstringSee Choosing a model. Defaults to auto.
streambooleanServer-sent events. See Streaming.
tools, tool_choicearray, string or objectSee Tool calling.
max_tokens / max_completion_tokensintegerEither works; converted to what each provider expects.
temperature, top_p, stop, seed, response_formatPassed 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.cost is 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}/cancel stops a batch (answers already in are kept), DELETE /batches/{id} cancels it and deletes its results, and GET /batches lists your batches (?limit=,?after=).
  • Only /v1/chat/completions batches are supported, answers are never streamed, andcallback_url is 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

HeaderMeaning
x-scoutcode-presetPreset that answered, e.g. gpt-oss-120b
x-scoutcode-providerProvider that answered, e.g. groq
x-scoutcode-modelProvider and model, e.g. groq/openai/gpt-oss-120b
x-scoutcode-attemptsHow many upstream attempts it took, including the one that worked
x-scoutcode-costWhat this request cost you, in USD
x-scoutcode-balanceWhat your account has left to spend after it, in USD (today's money plus your balance)
x-scoutcode-free-tierOn free-model answers: which free limits apply, starter or paying
x-scoutcode-free-left-minuteOn free-model answers: free requests left in the current minute
x-scoutcode-free-left-todayOn free-model answers: free requests left today (UTC)
Retry-AfterSeconds 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:

MoneyHow it works
Money per dayAn amount the operator gives your account each UTC day. Whatever is left at 00:00 UTC is gone, and you get a fresh amount.
BalanceMoney 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:

AccountFree-model requests
No money per day and an empty balance5/min, 100/day
Money per day, or a balance15/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:

StatusWhenRetry?
400Body isn't a JSON object or has no messagesNo, fix the request
413Body is over 25 MB (usually too many or too large base64 images)No, shrink the request
401Missing, invalid or disabled API keyNo
402 insufficient_quotaYour 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_exceededYour account used its free-model requests for now and can't pay for a paid modelAfter Retry-After
429 scoutcode_errorEvery candidate model is rate limited upstreamAfter Retry-After
4xx scoutcode_errorEvery model rejected the request (e.g. context too long, invalid tool schema). Carries the provider's status and its explanationNo, change the request
502Every attempt failed with server errors or timeoutsYes, with backoff
503The server has no usable models configuredContact 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/auto

Environment variables

OPENAI_BASE_URL=http://localhost:4321/v1
OPENAI_API_KEY=<your key>