CentroSky API Reference
Base URL: https://centrosky.com/v1
CentroSky is OpenAI-compatible. If you have code using the OpenAI SDK, change
the base URL and the API key and it works — including streaming.
What CentroSky adds on top: multi-agent orchestration. A team of
specialist agents can research, draft, review, and synthesize an answer
together, and you can watch each agent's progress.
Contents
- Authentication
- Quickstart
- Models
- Chat Completions
- Streaming
- Agent Orchestration (sync)
- Best-of-N Candidates
- Structured code output
- Async Tasks
- Webhooks
- Idempotency
- Rate Limits
- Errors
- SkyPower & Billing
Authentication
Every request needs a bearer token:
Authorization: Bearer csky_your_key_here
Create keys at centrosky.com → API. Keys are shown
once at creation. X-CentroSky-Key: csky_... is accepted as an alternative
header.
Never ship a key in client-side code — it carries your SkyPower balance.
Quickstart
cURL
curl https://centrosky.com/v1/chat/completions \
-H "Authorization: Bearer csky_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"model": "skymind-2",
"messages": [{"role": "user", "content": "Explain database indexes briefly"}]
}'Python (OpenAI SDK)
from openai import OpenAI
client = OpenAI(
api_key="csky_your_key_here",
base_url="https://centrosky.com/v1",
)
resp = client.chat.completions.create(
model="skymind-2",
messages=[{"role": "user", "content": "Explain database indexes briefly"}],
)
print(resp.choices[0].message.content)Node (OpenAI SDK)
import OpenAI from "openai";
const client = new OpenAI({
apiKey: "csky_your_key_here",
baseURL: "https://centrosky.com/v1",
});
const resp = await client.chat.completions.create({
model: "skymind-2",
messages: [{ role: "user", content: "Explain database indexes briefly" }],
});
console.log(resp.choices[0].message.content);Models
GET /v1/models
| Model | Description |
|---|---|
skymind-2 |
Default. Flagship. Health-aware routing across the strongest live backends. Add agents for orchestration. |
flash |
Fastest and cheapest. Good for classification, short replies, high volume. |
sp |
Balanced mid-tier. |
sp+ |
Legacy premium alias. |
skymind-2 resolves to a concrete backend at request time based on provider
health and prompt shape. The backend actually used is always reported in
centrosky.model_actual, so routing is never a black box.
Routing hints
{ "model": "skymind-2", "reasoning": true, "messages": [...] }reasoning: true routes to models stronger at multi-step problems.
task: "code" routes to the dedicated code pool, led by Codestral with the
strongest code-writing general backends behind it. Long prompts automatically
route to a long-context pool. The pool used is reported in
centrosky.routing_pool (general, reasoning, code, long-context).
Chat Completions
POST /v1/chat/completions
Request
| Field | Type | Default | Notes | |
|---|---|---|---|---|
model |
string | skymind-2 |
See Models | |
messages |
array | required | Standard OpenAI format | |
max_tokens |
int | 8192 | Total output budget, up to 131072. Budgets above a single backend's ceiling are automatically continued across backends until the answer is genuinely complete — see centrosky.segments. |
|
max_segments |
int | 8 | Max continuation segments (1–12). | |
temperature |
float | 0.7 | ||
stream |
bool | false | See Streaming | |
agents |
int \ | "auto" |
— | Enables orchestration. 1–20. |
reasoning |
bool | false | Routing hint | |
task |
string | — | "code" routes skymind-2 to the dedicated code pool (Codestral-led). |
|
model_prefer |
array | — | Backend id substrings to try first, e.g. ["deepseek"]. skymind-2 only. |
|
model_exclude |
array | — | Backend id substrings to never route to. A hard constraint — lets you act on model_actual telemetry. skymind-2 only. |
|
candidates |
int | — | 2–5. Best-of-N: N generations on N different backends; a reasoning-pool judge picks the winner. Non-streaming skymind-2 only. See Best-of-N. |
|
response_format |
string | — | "code_files" additionally parses fenced code blocks into structured centrosky.code_files. See Structured code output. |
Response
{
"id": "csky-3f9a...",
"object": "chat.completion",
"created": 1753243200,
"model": "skymind-2",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "An index is..." },
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 14,
"completion_tokens": 88,
"total_tokens": 102
},
"centrosky": {
"model_tier": "skymind-2",
"model_actual": "openai/gpt-oss-120b",
"provider": "groq",
"routing_pool": "general",
"spu_consumed": 0.102,
"latency_ms": 843,
"run_id": "3f9a..."
}
}The centrosky block is an extension. OpenAI SDKs ignore unknown fields, so
it never breaks compatibility.
Streaming
Set stream: true for token-by-token Server-Sent Events in OpenAI's
chat.completion.chunk format.
stream = client.chat.completions.create(
model="skymind-2",
messages=[{"role": "user", "content": "Write a haiku about databases"}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
print(delta.content, end="", flush=True)Raw wire format:
data: {"id":"csky-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"csky-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Silent"},"finish_reason":null}]}
data: {"id":"csky-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{...},"centrosky":{...}}
data: [DONE]Notes:
- The final chunk carries
usageandcentrosky— you get token counts
and SP spend without a second call.
- Errors that occur before streaming starts return a normal HTTP status
(402, 429, 503). Errors after the first byte arrive in-band as
{"error": {...}} followed by [DONE], because HTTP status can no longer
be changed once headers are sent.
- If a provider fails before any content is emitted, CentroSky silently fails
over to another. Once content has been sent it will not splice in a
different model mid-answer — the stream ends with
finish_reason: "length" instead.
Agent Orchestration (sync)
Add agents to any completion request:
curl https://centrosky.com/v1/chat/completions \
-H "Authorization: Bearer csky_your_key_here" \
-d '{
"model": "skymind-2",
"agents": 5,
"messages": [{"role":"user","content":"Design a rate limiter for a REST API"}]
}'"agents": "auto" lets the router choose the team size.
This is synchronous and can take minutes. For anything beyond a quick
run, use Async Tasks instead — long-held HTTP connections
fail on most proxies, mobile networks, and serverless platforms.
Response adds:
"centrosky": {
"mode": "agents",
"agent_count": 5,
"agents_used": ["architect", "coder", "reviewer", "..."],
"task_id": "9c2e...",
"spu_consumed": 4.21
}Best-of-N Candidates
Add candidates (2–5) to a non-streaming skymind-2 completion and the
gateway runs the request on that many different backends in one shot, then
has a reasoning-pool judge pick the best answer:
curl https://centrosky.com/v1/chat/completions \
-H "Authorization: Bearer csky_your_key_here" \
-d '{
"model": "skymind-2",
"task": "code",
"candidates": 3,
"max_tokens": 16000,
"messages": [{"role":"user","content":"Write a PHP rate limiter class with sliding window"}]
}'The winning answer comes back as a normal completion. The tournament is
reported under centrosky.candidates:
"centrosky": {
"mode": "candidates",
"candidates": {
"requested": 3,
"completed": 3,
"winner": 2,
"judge_model": "deepseek-reasoner",
"judge_reason": "Candidate 2 is the only complete implementation with correct window carry-over.",
"entries": [
{ "candidate": 1, "model": "codestral-latest", "tokens_out": 1420, "truncated": false, "winner": false },
{ "candidate": 2, "model": "openai/gpt-oss-120b", "tokens_out": 1610, "truncated": false, "winner": true },
{ "candidate": 3, "model": "deepseek-chat", "tokens_out": 1388, "truncated": false, "winner": false }
]
}
}Notes:
- Backend diversity is forced: each candidate excludes every model already
used, so you are genuinely comparing different models, not the same one
three times.
- You are billed for all N generations plus the judge —
usageand
spu_consumed cover the whole tournament. This is a quality dial, priced
accordingly.
model_excludeapplies to candidates and the judge alike.model_prefer
orders which backends get picked first.
- If some backends fail, the tournament runs with the survivors. If only one
survives, it wins by default. If none survive, the request falls back to a
normal single completion.
- Watch which models win your workloads over time (
entries[].winner) and
steer with model_prefer — the tournament generates its own routing data.
Structured code output
Add response_format: "code_files" to any completion and the gateway parses
fenced code blocks out of the answer server-side:
"centrosky": {
"code_files_count": 2,
"code_files": [
{ "path": "includes/RateLimiter.php", "language": "php", "content": "<?php ..." },
{ "path": "app.js", "language": "javascript", "content": "..." }
]
}Recognized fence formats: `php:path/file.php (language:path),
`path/file.php (bare path), and `php (bare language — a filename
is synthesized). If the model emits the same path twice, the last version
wins, so self-corrections replace earlier drafts. message.content is
unchanged — the structured files are additive, so existing parsers keep
working.
Combine with task: "code" and candidates for the full code-quality stack.
Async Tasks
The recommended path for orchestration. Submit, get a handle immediately,
poll or receive a webhook.
Create
POST /v1/mind/tasks → 202 Accepted
curl -X POST https://centrosky.com/v1/mind/tasks \
-H "Authorization: Bearer csky_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"agents": 5,
"messages": [{"role":"user","content":"Design a rate limiter for a REST API"}],
"webhook_url": "https://yourapp.com/hooks/centrosky"
}'| Field | Type | Notes | |
|---|---|---|---|
messages or prompt |
array \ | string | One is required |
agents |
int \ | "auto" |
1–20, default "auto" |
max_tokens |
int | Default 8192, total budget up to 131072 (continued across backends) | |
webhook_url |
string | Optional completion callback |
{
"id": "9c2e4f1a-...",
"object": "mind.task",
"status": "queued",
"poll_url": "/v1/mind/tasks/9c2e4f1a-...",
"centrosky": { "mode": "agents", "queue_position": 1 }
}Poll
GET /v1/mind/tasks/{id}
{
"id": "9c2e4f1a-...",
"object": "mind.task",
"status": "executing",
"progress": {
"percent": 45,
"steps_total": 5,
"steps_completed": 2,
"agents": [
{ "role": "architect", "model": "gpt-oss-120b", "status": "completed", "latency_ms": 2140 },
{ "role": "coder", "model": "gpt-oss-120b", "status": "running", "latency_ms": 0 }
]
},
"retry_after": 5
}Statuses: queued → executing → synthesizing → completed (or failed).
Respect Retry-After. It returns 5s normally, tightening to 2s past 80%.
Polling does not count against your rate limit.
On completion:
{
"status": "completed",
"result": { "role": "assistant", "content": "..." },
"usage": { "prompt_tokens": 210, "completion_tokens": 1840, "total_tokens": 2050 },
"centrosky": {
"spu_consumed": 4.21,
"latency_ms": 47200,
"models_used": ["..."],
"proof_hash": "a3f..."
}
}Cancel
DELETE /v1/mind/tasks/{id}
Only works while queued. A running task has already incurred upstream cost
and cannot be cancelled.
List
GET /v1/mind/tasks?limit=20
Polling example
import time, requests
H = {"Authorization": "Bearer csky_your_key_here"}
task = requests.post("https://centrosky.com/v1/mind/tasks",
headers=H, json={"agents": 5, "prompt": "Design a rate limiter"}).json()
while True:
r = requests.get(f"https://centrosky.com/v1/mind/tasks/{task['id']}", headers=H).json()
if r["status"] in ("completed", "failed"):
break
time.sleep(r.get("retry_after", 5))
print(r["result"]["content"] if r["status"] == "completed" else r["error"])Webhooks
Pass webhook_url when creating a task and CentroSky POSTs the result when
it finishes.
{
"event": "mind.task.completed",
"created": 1753243200,
"data": {
"id": "9c2e4f1a-...",
"object": "mind.task",
"status": "completed",
"result": { "role": "assistant", "content": "..." },
"usage": { "total_tokens": 2050 },
"centrosky": { "spu_consumed": 4.21 }
}
}Events: mind.task.completed, mind.task.failed.
Verifying signatures
Every delivery carries:
X-CentroSky-Signature: t=1753243200,v1=5f2c8a...
v1 is HMAC-SHA256(secret, "{t}.{raw_body}"). Always verify — otherwise
anyone who learns your endpoint can forge results.
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, sig = parts["t"], parts["v1"]
if abs(time.time() - int(ts)) > tolerance:
return False # stale — replay attempt
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)function verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(time() - (int)$p['t']) > $tolerance) return false;
$expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $p['v1']);
}Delivery: retried up to 5 times, then marked failed. Respond 2xx to
acknowledge. Redirects are not followed. Treat webhooks as at-least-once and
make your handler idempotent.
Idempotency
Send Idempotency-Key on any POST to make retries safe:
Idempotency-Key: 8f14e45f-ea1c-4d2b-9f0e-1a2b3c4d5e6f
Use a fresh UUID per logical operation.
| Situation | Result |
|---|---|
| First request | Executes normally, response cached |
| Replay after completion | Cached response + Idempotency-Replayed: true |
| Replay while still running | 409 |
| Same key, different body | 422 |
| Original failed | Not cached — retry executes normally |
Keys are scoped to your account and expire after 24 hours. Not supported on
streaming requests, which cannot be meaningfully replayed.
This matters most for agent runs: without a key, a client timeout plus an
automatic SDK retry means the work runs twice and you are billed twice.
Rate Limits
Per-minute request limits by plan. Every response includes:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 57 X-RateLimit-Reset: 1753243260
X-RateLimit-Reset is a Unix timestamp. On 429 you also get Retry-After
in seconds.
Agent runs cost more than one unit — an agents: 5 request consumes 5
units, because it does roughly five times the upstream work. Polling task
status costs nothing.
Partner keys. First-party integrations can be exempted from per-minute
limits entirely by setting rate_limit_rpm = -1 on the API key row
(centrosky_api_keys). SP metering still applies — this removes the request
throttle, not the billing. Recommended for pipelines that legitimately
multiply request volume (best-of-N candidates, multi-pass review chains).
Handle 429 with exponential backoff:
import time, requests
def call(payload, key, attempts=5):
for i in range(attempts):
r = requests.post("https://centrosky.com/v1/chat/completions",
headers={"Authorization": f"Bearer {key}"}, json=payload)
if r.status_code != 429:
return r
time.sleep(int(r.headers.get("Retry-After", 2 ** i)))
raise RuntimeError("rate limited after retries")Errors
Standard OpenAI-compatible envelope:
{
"error": {
"message": "Rate limit exceeded: 60 requests/min on your plan.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"retry_after": 12
}
}| HTTP | type |
Meaning | Action |
|---|---|---|---|
| 400 | invalid_request |
Malformed body | Fix the request |
| 401 | authentication_error |
Missing/invalid key | Check the key |
| 402 | insufficient_quota |
Out of SkyPower | Top up |
| 404 | not_found |
Unknown task id | Check the id |
| 409 | invalid_request |
Idempotency key in flight | Wait, then poll |
| 422 | invalid_request |
Idempotency key reused with new body | Use a fresh key |
| 429 | rate_limit_error |
Too many requests | Back off per Retry-After |
| 500 | server_error |
Internal failure | Retry with backoff |
| 502 | server_error |
Upstream provider failed | Retry |
| 503 | server_error |
No provider available | Retry shortly |
402 and 422 are not retryable without changing something. Everything
else is safe to retry with backoff.
SkyPower & Billing
Usage is metered in SkyPower (SP), paid in CENTRO (₵).
Every response reports consumption:
"centrosky": { "spu_consumed": 0.102 }Also exposed as headers on non-streaming completions:
X-SkyPower-Consumed: 0.102 X-Request-Id: 3f9a...
SP consumption scales with tokens and a per-model weight — flash costs less
per token than skymind-2. Agent runs consume roughly in proportion to team
size.
Plans, pack pricing, and current balance: centrosky.com/#/pricing
When SP runs out you get 402 with code: "sp_exhausted".
Support
- Dashboard: centrosky.com
- Pricing: centrosky.com/#/pricing
Include centrosky.run_id (or X-Request-Id) when reporting an issue —
it identifies the exact run.
Network SkyPower telemetry
GET /v1/network/skypower
Public aggregate telemetry for explorers and network-status displays. No authentication is required because the response contains no keys, prompts, wallets or user records.
Key sections:
capacity: theoretical, sustained, effective, burst and available SPdemand: current, 5-minute and 1-hour SP; 24-hour peak and completed SPUutilization: compute, concurrency, quota and request pressureproviders: aggregate health, slot counts and provider-type contributionoperations: queue depth, success rate and latency percentilesmeasurement: coverage and formulashistory: five-minute capacity and demand rollups for the last 24 hours
1 SP = 1 SPU/second.