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 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:

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 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

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-ratelimit-limit-requests, x-ratelimit-remaining-requestsYour key's per-minute limit and what's left. Sent with 429 responses.
x-ratelimit-limit-requests-day, x-ratelimit-remaining-requests-dayThe same for the daily limit
Retry-AfterSeconds 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:

AllowanceUsed by
Open-sourceauto, and presets that run on open-weight models (e.g. gpt-oss-120b)
FrontierPresets 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:

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
429 rate_limit_errorYour account is over its limit (or has no allowance yet)After 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>