API reference · v1

Squadron API

OpenAI-compatible endpoints on the same fleet as the console. If a client or SDK lets you set a base URL, it works with Squadron.

01 · Quickstart

Quickstart

The base URL is https://api.jmni.ai/v1. Keys are issued by the operator: write to [email protected] with what you're building.

Shell
export SQUADRON_KEY="sk-…"
curl https://api.jmni.ai/v1/models -H "Authorization: Bearer $SQUADRON_KEY"
Python · pip install openai
import os
from openai import OpenAI

client = OpenAI(base_url="https://api.jmni.ai/v1", api_key=os.environ["SQUADRON_KEY"])
stream = client.chat.completions.create(
    model="deepseek-v4.1-flash",
    messages=[{"role": "user", "content": "Status report in one line."}],
    stream=True,
)
for chunk in stream:
    if chunk.choices:
        print(chunk.choices[0].delta.content or "", end="", flush=True)
TypeScript · npm install openai
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.jmni.ai/v1", apiKey: process.env.SQUADRON_KEY });
const stream = await client.chat.completions.create({
  model: "deepseek-v4.1-flash",
  messages: [{ role: "user", content: "Status report in one line." }],
  stream: true,
});
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta?.content ?? "");

02 · Keys & limits

Keys & limits

Every key is named and carries its own model list, monthly budget, rate limits (requests per minute, tokens per minute, requests in parallel) and an expiry of at most 90 days. A new key starts at 30 requests a minute, 100,000 tokens a minute and two requests in parallel. When a key's budget is spent, its requests fail with a budget error until the budget resets.

The per-minute token limit also caps a single request. The gateway reserves each request's input plus its max_tokens before running it, so one call can't be larger than the key's per-minute token limit. Long-context work (hundreds of thousands of tokens) needs a key with a higher limit; ask for one.

Keep keys out of source control and shared documents. If one leaks, tell us and it is revoked immediately.

03 · Models

Models

ModelKindDetails
deepseek-v4.1-flashChat1,048,576-token context window. Reasons before answering.
glm-5.3-flashChat1,048,576-token context window. Reasons before answering; "reasoning_effort": "low" keeps that short.
embedEmbeddingsEmbeddingGemma 2. 768 dimensions, unit-length vectors. See Embeddings.

Both chat models run on the same fleet, one at a time. Ask for either: if the other one is loaded at that moment, it answers instead, so a request never fails just because of which model is loaded. The response's model field names the model that actually answered.

Reasoning

Both chat models think before they answer. The reasoning arrives in reasoning_content (on the message, or on each streamed delta), separate from content, and it counts toward max_tokens. Leave headroom: a tight max_tokens can be spent entirely on reasoning and return an empty answer. A cap of a few thousand tokens, or none at all, is a safe default.

04 · Streaming

Stream anything long

Set "stream": true for anything that might take a while; the examples above all do. Without streaming, a request has to complete within about 85 seconds, and the edge cuts connections that send nothing for around 100 seconds (HTTP 524). With streaming, tokens (reasoning first, then the answer) arrive as they're generated and the connection stays alive.

05 · Embeddings

Embeddings: search by meaning

An embedding turns a passage into 768 numbers, and passages that mean similar things land close together. That gives you search by meaning rather than keywords: the same machinery the console uses to answer from your documents. It's useful for:

  • Semantic search over notes, tickets, docs or email: "the renewal deadline" finds "notice is due 60 days before the term date".
  • Answering from your own material: find the best passages, then hand them to the chat model (below). This works on collections far larger than any context window.
  • Duplicates and related items: near-identical vectors mean near-identical content.
  • Tagging and routing: compare an item with a few labelled examples and take the closest.

Four rules that make it work

  1. Use task prefixes. EmbeddingGemma is trained with them:
    passages  title: {title or none} | text: {passage}
    searches  task: search result | query: {question}
    Other tasks take the same task: … | query: … shape: question answering, fact checking, classification, clustering, sentence similarity, code retrieval. Pick one scheme per index and never mix.
  2. Chunk before you embed. One vector per passage of a few paragraphs (roughly 200–500 words), with a little overlap. A whole document in one vector blurs together, and very long inputs are cut off.
  3. Vectors are unit length, so cosine similarity is a plain dot product. To store less, keep the first 512, 256 or 128 numbers and re-normalize; the model is trained for that. The gateway ignores the dimensions parameter, so truncate on your side.
  4. Batch. Send a list in input, dozens of passages per call.
Python · search your notes, then answer from them
import os
from openai import OpenAI

client = OpenAI(base_url="https://api.jmni.ai/v1", api_key=os.environ["SQUADRON_KEY"])

def embed(texts):
    return [d.embedding for d in client.embeddings.create(model="embed", input=texts).data]

notes = {
    "ring-maintenance": "Node S-3 logged three thermal warnings in the August load test. Schedule a fan swap.",
    "supplier-contract": "Renewal notice is due 60 days before the 1 March term date, in writing.",
    "onboarding": "New members get an invitation, then sign in with a one-time code sent by email.",
}
# documents and queries take different prefixes; keep them consistent across your index
doc_vecs = embed([f"title: {name} | text: {text}" for name, text in notes.items()])

def search(question, k=2):
    q = embed([f"task: search result | query: {question}"])[0]
    scored = [(sum(a * b for a, b in zip(q, d)), name) for d, name in zip(doc_vecs, notes)]
    return sorted(scored, reverse=True)[:k]  # unit vectors: dot product = cosine

question = "When do we have to tell the supplier we're renewing?"
hits = search(question)            # [(0.83, 'supplier-contract'), (0.65, 'ring-maintenance')]

context = "\n\n".join(notes[name] for _, name in hits)
answer = client.chat.completions.create(
    model="deepseek-v4.1-flash",
    max_tokens=2000,  # leave room: the model reasons before it answers
    messages=[
        {"role": "system", "content": "Answer from the context. If it isn't there, say so."},
        {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"},
    ],
)
print(answer.choices[0].message.content)  # "…in writing 60 days before the 1 March term date."

From there, store the vectors in whatever you already run (a NumPy array for thousands of passages; SQLite with sqlite-vec or Postgres with pgvector beyond that) and re-embed only what changes. Embedding calls count toward a key's request and token rates, but not its budget.

06 · Errors

Errors

StatusMeaning
400Malformed request, or an email address in an end-user field (user, metadata.user_id, safety_identifier). Usage is tracked per key: drop the field or send an opaque id.
401Missing or invalid key.
403That endpoint isn't available through the edge; see Endpoints.
429Rate limit: your key's, or the edge's (10 requests per 10 seconds per address). Back off and retry.
500, 503The model fleet is unreachable or overloaded; the message says which. Retry with backoff.
524A non-streamed request ran too long. Stream it.

07 · Endpoints

Endpoints

RoutePurpose
POST /v1/chat/completionsChat Completions
POST /v1/responsesResponses API
POST /v1/completionsLegacy text completions
POST /v1/embeddingsEmbeddings
GET /v1/modelsThe models your key may use
GET /health/livelinessGateway liveness; no key needed

Everything else, including key management, admin and metrics routes, is blocked at the edge with 403.

08 · Data handling

Data handling

Requests run on our own fleet, and no third-party AI provider sees them. The gateway records usage metadata (key, model, token counts, timing) and is configured not to log message content. The details, and how to check them, are in Claims.