Fetch predictions
A client for GET /predictions that fetches cached SNR quantiles (p50, p10, p90, in dB) for a list of link ids. The contract it respects:
link_idsis a repeated query parameter, capped at 100 per request (400 too_many_link_idsbeyond that), so larger fleets are chunked.- An empty
itemsarray is not an error: it means no cached predictions exist yet for those links. The client reports it and moves on; retry after the next capture cycle or uselive=truefor on-demand inference. 429 rate_limitedis retried afterRetry-After; other4xxresponses stop the client.
Set CONSTELLATION_API_BASE and CONSTELLATION_API_TOKEN first (common setup).
- TypeScript
- Python
- Go
- cURL
const BASE = process.env.CONSTELLATION_API_BASE ?? 'https://api.constellation.space'
const TOKEN = process.env.CONSTELLATION_API_TOKEN
if (!TOKEN) throw new Error('CONSTELLATION_API_TOKEN is not set')
const LINK_IDS = ['SAT-0012:GS-SEA-01', 'SAT-0013:GS-SVL-02']
const CHUNK = 100
type Prediction = {
link_id: string
horizon_minutes: number
model_version: string
value: { p50: number; p10: number; p90: number }
}
type PredictionSet = { model_family: string; captured_at: string; items: Prediction[] }
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
async function fetchChunk(linkIds: string[], attempt = 0): Promise<PredictionSet> {
const url = new URL(`${BASE}/predictions`)
url.searchParams.set('model_family', 'snr')
url.searchParams.set('horizon_minutes', '15')
for (const id of linkIds) url.searchParams.append('link_ids', id)
const res = await fetch(url, { headers: { 'x-api-key': TOKEN! } })
if (res.status === 429) {
if (attempt >= 5) throw new Error('giving up after 5 rate-limited retries')
await sleep(Number(res.headers.get('retry-after') ?? '5') * 1000)
return fetchChunk(linkIds, attempt + 1)
}
if (!res.ok) throw new Error(`predictions read failed: ${res.status} ${await res.text()}`)
return (await res.json()) as PredictionSet
}
for (let i = 0; i < LINK_IDS.length; i += CHUNK) {
const set = await fetchChunk(LINK_IDS.slice(i, i + CHUNK))
if (set.items.length === 0) {
console.log(`no cached predictions yet for this chunk (set captured_at ${set.captured_at}); ` +
'retry after the next capture or request live=true')
continue
}
for (const p of set.items) {
console.log(`${p.link_id} +${p.horizon_minutes}m ` +
`p50 ${p.value.p50} dB (p10 ${p.value.p10}, p90 ${p.value.p90}) [${p.model_version}]`)
}
}
import os
import time
import requests
BASE = os.environ.get("CONSTELLATION_API_BASE", "https://api.constellation.space")
TOKEN = os.environ["CONSTELLATION_API_TOKEN"]
LINK_IDS = ["SAT-0012:GS-SEA-01", "SAT-0013:GS-SVL-02"]
CHUNK = 100
def fetch_chunk(link_ids, attempt=0):
params = [
("model_family", "snr"),
("horizon_minutes", "15"),
] + [("link_ids", link_id) for link_id in link_ids]
resp = requests.get(
f"{BASE}/predictions",
params=params,
headers={"x-api-key": TOKEN},
timeout=30,
)
if resp.status_code == 429:
if attempt >= 5:
raise RuntimeError("giving up after 5 rate-limited retries")
time.sleep(float(resp.headers.get("Retry-After", "5")))
return fetch_chunk(link_ids, attempt + 1)
resp.raise_for_status()
return resp.json()
for start in range(0, len(LINK_IDS), CHUNK):
pred_set = fetch_chunk(LINK_IDS[start : start + CHUNK])
if not pred_set["items"]:
print(f"no cached predictions yet (set captured_at {pred_set['captured_at']}); "
"retry after the next capture or request live=true")
continue
for p in pred_set["items"]:
v = p["value"]
print(f"{p['link_id']} +{p['horizon_minutes']}m "
f"p50 {v['p50']} dB (p10 {v['p10']}, p90 {v['p90']}) [{p['model_version']}]")
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
const chunk = 100
type Prediction struct {
LinkID string `json:"link_id"`
HorizonMinutes int `json:"horizon_minutes"`
ModelVersion string `json:"model_version"`
Value map[string]float64 `json:"value"`
}
type PredictionSet struct {
ModelFamily string `json:"model_family"`
CapturedAt string `json:"captured_at"`
Items []Prediction `json:"items"`
}
func fetchChunk(base, token string, linkIDs []string) (*PredictionSet, error) {
q := url.Values{}
q.Set("model_family", "snr")
q.Set("horizon_minutes", "15")
for _, id := range linkIDs {
q.Add("link_ids", id)
}
for attempt := 0; ; attempt++ {
req, err := http.NewRequest(http.MethodGet, base+"/predictions?"+q.Encode(), nil)
if err != nil {
return nil, err
}
req.Header.Set("x-api-key", token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusTooManyRequests {
resp.Body.Close()
if attempt >= 5 {
return nil, fmt.Errorf("giving up after 5 rate-limited retries")
}
wait, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
if wait <= 0 {
wait = 5
}
time.Sleep(time.Duration(wait) * time.Second)
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("predictions read failed: %d", resp.StatusCode)
}
var set PredictionSet
if err := json.NewDecoder(resp.Body).Decode(&set); err != nil {
return nil, err
}
return &set, nil
}
}
func main() {
base := os.Getenv("CONSTELLATION_API_BASE")
if base == "" {
base = "https://api.constellation.space"
}
token := os.Getenv("CONSTELLATION_API_TOKEN")
if token == "" {
log.Fatal("CONSTELLATION_API_TOKEN is not set")
}
linkIDs := []string{"SAT-0012:GS-SEA-01", "SAT-0013:GS-SVL-02"}
for start := 0; start < len(linkIDs); start += chunk {
end := min(start+chunk, len(linkIDs))
set, err := fetchChunk(base, token, linkIDs[start:end])
if err != nil {
log.Fatal(err)
}
if len(set.Items) == 0 {
fmt.Printf("no cached predictions yet (set captured_at %s); "+
"retry after the next capture or request live=true\n", set.CapturedAt)
continue
}
for _, p := range set.Items {
fmt.Printf("%s +%dm p50 %.1f dB (p10 %.1f, p90 %.1f) [%s]\n",
p.LinkID, p.HorizonMinutes,
p.Value["p50"], p.Value["p10"], p.Value["p90"], p.ModelVersion)
}
}
}
#!/usr/bin/env bash
set -euo pipefail
BASE="${CONSTELLATION_API_BASE:-https://api.constellation.space}"
: "${CONSTELLATION_API_TOKEN:?set CONSTELLATION_API_TOKEN}"
# Repeat link_ids per link; at most 100 per request.
QUERY="model_family=snr&horizon_minutes=15"
QUERY+="&link_ids=SAT-0012:GS-SEA-01"
QUERY+="&link_ids=SAT-0013:GS-SVL-02"
for attempt in 1 2 3 4 5; do
response=$(curl -sS -w '\n%{http_code}' \
-H "x-api-key: $CONSTELLATION_API_TOKEN" \
"$BASE/predictions?$QUERY")
status=$(tail -n1 <<<"$response")
body=$(sed '$d' <<<"$response")
if [[ "$status" == "429" ]]; then
wait=$(jq -r '.retry_after_seconds // 5' <<<"$body")
echo "rate limited, waiting ${wait}s (attempt $attempt)" >&2
sleep "$wait"
continue
fi
if [[ "$status" != "200" ]]; then
echo "predictions read failed ($status): $body" >&2
exit 1
fi
if [[ $(jq '.items | length' <<<"$body") == "0" ]]; then
echo "no cached predictions yet (set captured_at $(jq -r '.captured_at' <<<"$body"));"
echo "retry after the next capture or request live=true"
else
jq -r '.items[] |
"\(.link_id) +\(.horizon_minutes)m p50 \(.value.p50) dB (p10 \(.value.p10), p90 \(.value.p90)) [\(.model_version)]"' \
<<<"$body"
fi
exit 0
done
echo "giving up after 5 rate-limited retries" >&2
exit 1
Expected output:
SAT-0012:GS-SEA-01 +15m p50 12.4 dB (p10 9.1, p90 15.8) [snr-v11]
SAT-0013:GS-SVL-02 +15m p50 8.2 dB (p10 5.6, p90 11.3) [snr-v11]
Treat p10 as the planning floor for link-closure decisions; a wide p10 to p90 spread means the model is uncertain. To get on-demand inference with full provenance instead of the cached set, add live=true to the query and parse the InferencePayload shape (Predictions API reference); the sandbox environment is the natural place to exercise it.
Related
- Predictions API reference: parameters, cached versus live shapes, provenance.
- Integration patterns: polling cadence for cached predictions.