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.
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
| Model | Kind | Details |
|---|---|---|
deepseek-v4.1-flash | Chat | 1,048,576-token context window. Reasons before answering. |
glm-5.3-flash | Chat | 1,048,576-token context window. Reasons before
answering; "reasoning_effort": "low" keeps that short. |
embed | Embeddings | EmbeddingGemma 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
- 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 sametask: … | query: …shape:question answering,fact checking,classification,clustering,sentence similarity,code retrieval. Pick one scheme per index and never mix. - 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.
- 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
dimensionsparameter, so truncate on your side. - Batch. Send a list in
input, dozens of passages per call.
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
| Status | Meaning |
|---|---|
| 400 | Malformed 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. |
| 401 | Missing or invalid key. |
| 403 | That endpoint isn't available through the edge; see Endpoints. |
| 429 | Rate limit: your key's, or the edge's (10 requests per 10 seconds per address). Back off and retry. |
| 500, 503 | The model fleet is unreachable or overloaded; the message says which. Retry with backoff. |
| 524 | A non-streamed request ran too long. Stream it. |
07 · Endpoints
Endpoints
| Route | Purpose |
|---|---|
POST /v1/chat/completions | Chat Completions |
POST /v1/responses | Responses API |
POST /v1/completions | Legacy text completions |
POST /v1/embeddings | Embeddings |
GET /v1/models | The models your key may use |
GET /health/liveliness | Gateway 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.