Drive Coord Desk from your own code
Everything the web page does is available over HTTP: post one paste of genomic coordinates — BED
intervals, a headered table, VCF records, GFF3/GTF features, SAM alignments, a Picard
interval_list or region strings — say what you are converting it to, and send the
facts the free engine computed from it. What comes back is a senior
bioinformatician's review: which basis the source really is in, the shift to the
target as two integers, what the assembly evidence supports, an answer to every engine flag, the
issues with their fixes, the conversion as runnable commands, what to check afterwards, and a
verdict of safe_to_convert, convert_with_fixes or
stop_and_check.
The natural use is a pipeline step that refuses to hand a coordinate file to the next tool until
its conventions have been checked — the two failures the app exists to prevent are silent ones: an
off-by-one shift between 0-based half-open and 1-based closed, and positions or contig names from
one assembly used against another. One thing is different from most apps on this platform, and it
is the whole of step 4: the arithmetic is not the model's. A free, deterministic
engine — the same coords.js the browser runs — parses the paste, decides each
format's basis, puts every record on one axis, scores the assembly evidence, normalises VCF
alleles, finds off-by-one pairs, previews the conversion and raises the flags. The model is told to
quote that object, never to recompute it. An API caller should produce it too.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses
the same envelope, so one helper covers the whole API:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "details": { ... } } }
Send your token as Authorization: Bearer … on every call. The app slug travels in the
body of /guest as {"slug": "coord-desk"}; after that the token
itself carries the app, so a run needs only Authorization,
Content-Type: application/json and the Idempotency-Key described in
step 5. There is no slug header.
The request body for /estimate, /run and /run-stream is the
input object itself — not wrapped in anything. A body of {"input": {…}} returns 200
and quietly hides every field from the model, so a run that looks fine comes back reviewing
nothing. Refuse to send anything that is not a plain JSON object: /estimate happily
prices a bare string, a number, null and [] alike.
Error codes
| code | status | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Get a new one from the token page. |
payment_required | 402 | The balance is below min_credits, or a guest token tried a metered run. Call /estimate first, then sign in and top up. |
validation_error | 400 | The body is not a plain JSON object, or a field is the wrong type — every field of this app's input is a string, facts included. A facts object sent as an object rather than as a JSON string is the common one. |
not_found | 404 | Unknown job id, or the app slug does not exist. Check the id you polled with. |
rate_limited | 429 | Too many requests. Back off and retry; do not tight-loop a poll. |
internal | 500 | A server-side failure. Retry with the same Idempotency-Key so you are not billed twice. |
Replaying an Idempotency-Key with a changed body is not a retry and is
rejected rather than billed. When you change the input — an edited paste, another
target, a different assembly — bump the attempt suffix on the key instead.
The field to get right first: task
Coord Desk has exactly one task, and task is always "review". Send it as
the first field of every request body. There is no second lane to choose and nothing to chain: one
paste in, one review out.
task | what comes back |
|---|---|
"review" | One JSON object with "lane": "review": a title and summary, the verdict with its reason, the source (format, basis, the start/end shift to the target and whether that agrees with the engine), the assembly call, one coverage entry per engine flag, the issues, the conversion steps as commands, the checks_after list and notes_on_input. |
A missing or different task is not an error: the model does the review anyway and says
so in notes_on_input. Do not rely on that — a script that sends
"review" gets a clean notes_on_input and can treat anything written there
as a real remark about the input.
The input
The input is a JSON object — never a bare string, never wrapped in an
input key — and every one of its fields is a string, because the
transport takes scalars only. That is why facts is a JSON string rather than a nested
object.
| field | type | meaning |
|---|---|---|
task | string, required | Always "review". |
records | string, required | The paste, verbatim: BED; a headered table (the UCSC table browser's #chrom / chromStart / chromEnd, MAF's Start_Position, a hand-made chr / start / end table); VCF; GFF3 or GTF; SAM; a Picard interval_list; or region strings chr:start-end. Formats may be mixed in one paste. Clipped to 24,000 characters — see Clipping. |
target | enum string | What you are converting to: "bed" (0-based half-open), "region" (1-based closed chr:start-end), "gff" (GFF3, 1-based closed) or "interval_list" (Picard, 1-based closed, with an @SQ header). Default "bed". |
contig_style | enum string | The contig naming you want out: "keep", "ucsc" (chr1, chrM) or "ensembl" (1, MT). Default "keep". |
source_basis | enum string | "auto", or your override for BED-shaped and table input only: "zero" or "one". VCF, GFF/GTF, SAM, interval_list and region strings are 1-based by specification and ignore it. Default "auto". |
assembly | enum string | What you say the data is on: "unknown", "GRCh37" or "GRCh38". The engine checks it against the file's own evidence and flags a disagreement. Default "unknown". |
pipeline_hint | string, optional | Where the output goes next, free text, at most 300 characters — "GATK -L targets.list", "bedtools intersect with a GRCh38 panel", "CrossMap to hg38". It decides whether a real problem is a blocker or a footnote. |
facts | string, strongly recommended | The engine's digest of the paste, JSON.stringify-ed. Shape and the script that computes it are in step 4. Without it the model reviews the paste alone, replies coverage: [], sets both agrees_with_engine fields to false and says in notes_on_input that nothing was cross-checked. |
retry_note | string, optional | Only on a reformat retry, when a previous reply did not parse: a sentence naming the failure. The model is told to obey it. Bump the attempt suffix on the key when you send one. |
A worked example
A UCSC table-browser export headed for a GRCh37 GATK run that wants Ensembl names and region strings. The second record already uses an Ensembl name — the kind of mix that silently drops rows out of an intersect:
{
"task": "review",
"records": "#chrom\tchromStart\tchromEnd\tname\nchr1\t11868\t12227\texon1\n1\t69090\t70008\torf1",
"target": "region",
"contig_style": "ensembl",
"source_basis": "auto",
"assembly": "GRCh37",
"pipeline_hint": "GATK -L targets.list",
"facts": "{\"engine\":\"coord-desk/1\",\"clipped\":null,\"lines_total\":3,\"records\":2, ... }"
}
For this paste the engine reads a two-record table whose basis is decided by its
header names (the column is named chromStart
, so 0-based half-open), states the
shift to region as shift_start: 1, shift_end: 0, previews
1:11869-12227 and 1:69091-70008, finds no assembly evidence beyond your
declaration, and raises one flag — F-001 mixed-contig-style, severity
warn. The reply must answer F-001 in coverage and nothing
else.
Clipping
The page sends at most 24,000 characters of records. Header lines
(#, @, track, browser) before the first data line
are kept whole; then whole data lines are taken alternately from both ends, and the middle is
dropped with one marker line in its place. It never cuts inside a line:
# [... 1840 lines (61233 characters) cut from the middle ...]
Clip the same way if you send more — CDCoords.clipMiddle(text, 24000) does exactly
this — and keep the marker: the prompt keys on it and says so in notes_on_input. The
counts travel in the facts as facts.clipped = {"chars_cut": 61233, "lines_cut": 1840}
(it is null when nothing was cut). The facts are computed on the clipped text —
the same text the model sees — so line numbers in the facts, and the line numbers the reply
cites, are line numbers of records as sent (the marker counts as a line). On an
unclipped paste that is simply your paste.
The facts object
In the browser this object is computed for free, before the run, by /coords.js
(window.CDCoords). The system prompt tells the model it is correct arithmetic and must
not be re-done differently: if the facts say line 7 is [99, 102), it is. Its keys:
| key | what it holds |
|---|---|
engine | The engine version, "coord-desk/1". |
clipped | null, or {chars_cut, lines_cut} when the paste was clipped. |
lines_total, records, unreadable_lines | Lines in the paste, coordinate records read, and lines that fit no supported format. |
by_format | Record counts per format: bed, table, vcf, gff, sam, interval_list, region. |
basis[] | One entry per format: {format, basis, decided_by, evidence[], records}, where decided_by is spec, header names, evidence, assumed or override. |
contig_styles, contigs | Counts per naming style (ucsc, ensembl, …) and the first 30 contigs in the order they appear. |
assembly | {call, confidence, declared_by_user, scores, conflict, evidence[]} — the engine's call, how sure it is, what you declared, the per-assembly scores (GRCh37, GRCh38), whether the evidence conflicts, and up to ten pieces of evidence {points_to, kind, detail, line} (header lengths, reference lines, alt-contig names, RefSeq versions, positions past a chromosome end). |
variants | null without VCF records; otherwise the normalisation results: multi-allelic count, non-minimal alleles with their minimal form, no-ops, duplicates, indels whose left-alignment cannot be checked, symbolic alleles. |
off_by_one_pairs | Up to six pairs of records in different formats that sit exactly one base apart. |
target | {format, label, contig_style, shift_from_main_basis, shift_start, shift_end} — the shift from the dominant source basis to the target, as text and as two integers. |
conversion | {lines, zero_length, not_renamed, mito_renamed, skipped_unmapped, invalid, preview} — the engine's own conversion, with the first eight output lines in preview. |
sample_records | The first twelve records on the engine's internal axis: {line, format, contig, start0, end0} (0-based half-open), plus pos/ref/alt for VCF, start1 for 1-based formats, and name. |
flags[] | {id, rule, severity, message, lines} with ids F-001, F-002, … in order, severity error, warn or info, and up to twelve paste line numbers. |
The flag rules
So a script can filter on them. Severity is fixed unless the table says otherwise.
| rule | severity | raised when |
|---|---|---|
truncated | error | The paste is longer than the 100,000 lines the engine reads; the checks and the conversion cover the first part only. |
parse-error | error | Lines could not be read as coordinates in any supported format. |
negative-start | error | A record has a negative start; no convention allows it. |
start-after-end | error | A record ends before it starts once put on one axis. |
zero-in-one-based | error | A 0 sits in a 1-based start column; the file was probably written 0-based. |
basis-assumed | info | Headerless BED-shaped lines were read as BED (0-based) with nothing to prove it either way. |
looks-one-based | warn | BED-shaped records with start == end suggest 1-based single bases; read as 1-based closed. |
zero-length | warn | Empty BED records (start == end): an insertion site, or more often a 1-based position written into BED. |
basis-ambiguous | warn | A table's start column name does not state its basis; read as 1-based closed. |
override-contradicted | error | source_basis says 1-based but records start at 0. |
override-vs-header | warn | source_basis disagrees with the basis the table header names. |
mixed-formats | info | The paste mixes formats; each goes on the axis by its own convention. |
off-by-one-pair | error | Records in different formats sit exactly one base apart: one side was converted without its shift. |
mixed-contig-style | warn | Contig names mix styles (chr1 and 1); the minority drops out of any intersect. |
contig-not-in-header | error | Records use contigs the ##contig / @SQ / ##sequence-region header does not declare. |
beyond-length | warn or error | Records end past a chromosome's length on one assembly (error when that is the declared one) or on both. |
assembly-conflict | error | The file's evidence points at both GRCh37 and GRCh38. |
assembly-declared-conflict | error | Your assembly disagrees with the file's own evidence. |
assembly-unknown | info | Nothing names or implies an assembly and you declared none. |
t2t-assembly | warn | The header names T2T-CHM13; length checks are off. |
non-primary-contig | info | Alt, patch, decoy, unplaced or non-human contigs with no counterpart under a rename. |
multiallelic | warn | VCF records carry more than one ALT; split them (bcftools norm -m-). |
non-minimal | warn | Alleles are not in minimal form, so the same variant written two ways will not match. |
noop-allele | warn | An ALT is identical to REF after trimming. |
duplicate-variant | warn | A record repeats a variant already seen once normalised. |
left-align-unverified | info | Indels cannot be checked for left-alignment without the reference (bcftools norm -f ref.fa). |
symbolic-allele | info | Symbolic or breakend alleles; the interval is POS-1 to INFO/END. |
allele-case | warn | REF characters outside ACGTN, or lowercase (soft-masked) bases. |
unmapped-read | info | SAM records are unmapped and have no interval. |
bad-strand | warn | A BED sixth column is not +, - or .; the columns may be shifted. |
unsorted | info | Records come before an earlier start on the same contig. |
duplicate-interval | info | An interval repeats an earlier one exactly. |
zero-length-target | warn | Empty intervals cannot be written as a closed 1-based range; written GFF3-style. |
not-renamed | warn | Contigs kept their names under the rename because no standard mapping exists. |
chrM-rename | info or error | chrM and MT were renamed; an error on GRCh37/hg19, where hg19's chrM (16,571 bp) is not the rCRS (16,569 bp). |
no-contig-length | warn | An interval_list header needs a contig length the engine does not know; written as LN:0. |
not-converted | error | Records were left out of the conversion because the interval is invalid. |
1. Get a token
The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. Nothing on that page needs a developer tool — it reads the same storage the app itself uses and prints the token for you.
A guest token can call /me and /estimate. The review is
metered, so a run needs a personal token from signing in. Nothing about the engine
is metered: the facts in step 4 cost nothing and need no token at all.
# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
# https://coord-desk.skillsafe.ai/tokens.html
# export SKILLSAFE_TOKEN="aut_YOUR_TOKEN"
#
# To mint a guest token from the command line instead. A guest token is enough for
# /me and /estimate; a review run needs a personal token.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug": "coord-desk"}'
# {"ok":true,"data":{"token":"aut_...","subject_type":"guest"}}
# Open https://coord-desk.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot run a review.
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest",
data=json.dumps({"slug": "coord-desk"}).encode(),
method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
print(TOKEN)
// Open https://coord-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a review.
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "coord-desk" }),
});
const GUEST_TOKEN = (await res.json()).data.token;
// Open https://coord-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a review.
guestReq, _ := http.NewRequest(http.MethodPost,
"https://api.skillsafe.ai/v1/app-api/guest",
bytes.NewReader([]byte(`{"slug": "coord-desk"}`)))
guestReq.Header.Set("Content-Type", "application/json")
guestRes, err := http.DefaultClient.Do(guestReq)
if err != nil {
panic(err)
}
defer guestRes.Body.Close()
var guest struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
_ = json.NewDecoder(guestRes.Body).Decode(&guest)
fmt.Println(guest.Data.Token)
// Open https://coord-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a review.
var http = HttpClient.newHttpClient();
var guestReq = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\": \"coord-desk\"}"))
.build();
HttpResponse<String> guest = http.send(guestReq, HttpResponse.BodyHandlers.ofString());
System.out.println(guest.body()); // {"ok":true,"data":{"token":"aut_...","subject_type":"guest"}}
# Open https://coord-desk.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot run a review.
require "json"
require "net/http"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.generate({ "slug" => "coord-desk" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
GUEST_TOKEN = JSON.parse(res.body)["data"]["token"]
<?php
// Open https://coord-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a review.
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["slug" => "coord-desk"]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
$GUEST_TOKEN = $payload["data"]["token"];
// Open https://coord-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a review.
using System.Net.Http.Json;
using System.Text.Json;
var http = new HttpClient();
var guestRes = await http.PostAsJsonAsync(
"https://api.skillsafe.ai/v1/app-api/guest",
new { slug = "coord-desk" });
var guest = await guestRes.Content.ReadFromJsonAsync<JsonElement>();
var guestToken = guest.GetProperty("data").GetProperty("token").GetString();
2. A tiny client
One helper that adds the headers, unwraps data and raises on error. Every later sample builds on it.
# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="coord-desk"
TOKEN="$SKILLSAFE_TOKEN" # from https://coord-desk.skillsafe.ai/tokens.html
call() { # call <path> [json-body]
if [ -n "$2" ]; then
curl -sS -X POST "$BASE/$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
else
curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN"
fi
}
import hashlib, json, os, time, urllib.error, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "coord-desk"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://coord-desk.skillsafe.ai/tokens.html
def call(path, body=None, headers=None):
"""Returns the unwrapped `data`, or raises with the API error code."""
if body is not None and not isinstance(body, dict):
raise TypeError("the request body must be a JSON object, not a bare string")
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(f"{BASE}/{path}", data=data,
method="POST" if body is not None else "GET")
req.add_header("Authorization", f"Bearer {TOKEN}")
if body is not None:
req.add_header("Content-Type", "application/json")
for k, v in (headers or {}).items():
req.add_header(k, v)
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as e:
payload = json.load(e)
if not payload.get("ok"):
err = payload.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
return payload["data"]
// Node 18+ (global fetch). Paste the token from /tokens.html, or load it from
// your own secret store - never commit it.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "coord-desk";
const TOKEN = "YOUR_TOKEN";
async function call(path, body, extraHeaders) {
if (body !== undefined && (body === null || typeof body !== "object" || Array.isArray(body))) {
throw new TypeError("the request body must be a JSON object, not a bare string");
}
const res = await fetch(`${BASE}/${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {}),
...(extraHeaders || {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const payload = await res.json();
if (!payload.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
return payload.data;
}
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"strings"
"time"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "coord-desk"
)
var token = os.Getenv("SKILLSAFE_TOKEN") // from https://coord-desk.skillsafe.ai/tokens.html
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
// call returns the unwrapped `data` as raw JSON; decode it into the struct you need.
func call(path string, body map[string]string, hdr map[string]string) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if body != nil {
method = http.MethodPost
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+"/"+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
for k, v := range hdr {
req.Header.Set(k, v)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
// Java 17+, with Jackson (com.fasterxml.jackson.core:jackson-databind) for JSON.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.*;
import java.util.Map;
public class CoordDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "coord-desk";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static final ObjectMapper MAPPER = new ObjectMapper();
/** Returns the unwrapped data node, or throws with the API error code. */
static JsonNode call(String path, Map<String, String> body, Map<String, String> extra) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN);
if (body != null) {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(body)));
} else {
b.GET();
}
if (extra != null) extra.forEach(b::header);
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
JsonNode payload = MAPPER.readTree(res.body());
if (!payload.path("ok").asBoolean(false)) {
JsonNode e = payload.path("error");
throw new RuntimeException(e.path("code").asText() + ": " + e.path("message").asText());
}
return payload.get("data");
}
}
require "digest"
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "coord-desk"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://coord-desk.skillsafe.ai/tokens.html
def call(path, body = nil, extra = {})
raise TypeError, "the request body must be a JSON object" if body && !body.is_a?(Hash)
uri = URI("#{BASE}/#{path}")
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
extra.each { |k, v| req[k] = v }
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "coord-desk";
define("TOKEN", getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"); // from /tokens.html
function call(string $path, ?array $body = null, array $extra = []) {
$ch = curl_init(BASE . "/" . $path);
$headers = array_merge(["Authorization: Bearer " . TOKEN], $extra);
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
// The body must encode as an object: an empty PHP array would encode
// as [] and be rejected as a validation_error.
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_SLASHES));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
static class CoordDesk
{
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public const string Slug = "coord-desk";
public static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
public static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(string path, Dictionary<string, string>? body = null,
Dictionary<string, string>? extra = null)
{
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post,
$"{Base}/{path}");
req.Headers.Add("Authorization", $"Bearer {Token}");
foreach (var (k, v) in extra ?? new()) req.Headers.Add(k, v);
if (body is not null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var payload = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!payload.GetProperty("ok").GetBoolean())
{
var e = payload.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return payload.GetProperty("data");
}
}
3. Check the session and the balance
GET /me tells you whether the token is a guest or a person, and what the balance is:
subject_type — "user" or "guest" — plus
credits, the wallet balance, and a subject id. There is no username in it, so
"signed in" is subject_type === "user" and nothing else; a guest can
price a review but cannot start one. Compare credits against
min_credits from the next step before you run, so a shortfall surfaces as your own
clear message rather than a 402.
call me
# {"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":51234}}
# A guest token answers the same call with subject_type "guest" and cannot run
# a review. Gate on it before you spend a poll loop finding out:
call me | grep -q '"subject_type":"user"' || {
echo "sign in at https://coord-desk.skillsafe.ai/tokens.html first" >&2
exit 1
}
me = call("me")
signed_in = me["subject_type"] == "user"
print(me["subject_type"], me.get("credits"), "signed in" if signed_in else "guest")
if not signed_in:
raise SystemExit("a guest token can price a review but cannot run one")
const me = await call("me");
const signedIn = me.subject_type === "user";
console.log(me.subject_type, me.credits, signedIn ? "signed in" : "guest");
if (!signedIn) throw new Error("a guest token can price a review but cannot run one");
raw, err := call("me", nil, nil)
if err != nil {
log.Fatal(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
if me.SubjectType != "user" {
log.Fatal("a guest token can price a review but cannot run one")
}
JsonNode me = CoordDesk.call("me", null, null);
System.out.println(me.path("subject_type").asText() + " " + me.path("credits").asLong());
// "signed in" is subject_type.equals("user") - there is no username field.
if (!"user".equals(me.path("subject_type").asText())) {
throw new IllegalStateException("a guest token can price a review but cannot run one");
}
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
abort "sign in first - a guest token cannot run a review" unless me["subject_type"] == "user"
<?php
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
if ($me["subject_type"] !== "user") {
throw new RuntimeException("a guest token can price a review but cannot run one");
}
var me = await CoordDesk.Call("me");
var signedIn = me.GetProperty("subject_type").GetString() == "user";
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")} {signedIn}");
if (!signedIn) throw new Exception("a guest token can price a review but cannot run one");
4. Compute the facts, then price the run — both free
Computing the facts outside the browser. The engine is one plain script served by
this host, /coords.js, and it runs unmodified in Node: set
global.window = {}, require it, and call
CDCoords.analyze() then CDCoords.toFacts(). No dependencies, no token,
no charge. The recipe the page uses is exactly:
clip = CDCoords.clipMiddle(text, 24000)
facts = JSON.stringify(CDCoords.toFacts(
CDCoords.analyze(clip.text, { target, style: contig_style, basis: source_basis, assembly }),
clip))
body.records = clip.text // the clipped paste, marker line included
body.facts = facts // a STRING, not an object
Note the option names: the engine takes style and basis, the request
body calls them contig_style and source_basis. Pass the same values to
both, or the facts describe a different conversion from the one you asked the model about. The
cURL tab writes a ten-line facts.js that prints {records, facts} ready to
drop into the body; the other languages either load the engine in-process (JavaScript) or drive
that script as a subprocess, which is both the shortest route and the only way to be certain your
facts are the ones the app would have produced.
# The paste, saved as in.txt - the worked example used everywhere below.
printf '#chrom\tchromStart\tchromEnd\tname\nchr1\t11868\t12227\texon1\n1\t69090\t70008\torf1\n' > in.txt
# The engine, fetched once and kept next to your script. It is the same file the
# web page loads; re-fetch it when the app is released again.
curl -sS -o coords.js "https://coord-desk.skillsafe.ai/coords.js"
# facts.js - no dependencies, no token, no charge.
# usage: node facts.js <file> [target] [contig_style] [source_basis] [assembly]
cat > facts.js <<'JS'
const fs = require("fs");
global.window = {}; // coords.js attaches CDCoords here
require("./coords.js");
const C = window.CDCoords;
const [file, target = "bed", style = "keep", basis = "auto", assembly = "unknown"] = process.argv.slice(2);
const text = fs.readFileSync(file, "utf8").replace(/\r\n?/g, "\n");
const clip = C.clipMiddle(text, C.MAX_TEXT); // 24,000 chars, whole lines, marker kept
const facts = C.toFacts(C.analyze(clip.text, { target, style, basis, assembly }), clip);
process.stdout.write(JSON.stringify({ records: clip.text, facts: JSON.stringify(facts) }));
JS
node facts.js in.txt region ensembl auto GRCh37 > engine.json
head -c 160 engine.json
# {"records":"#chrom\tchromStart\tchromEnd\tname\nchr1\t11868...","facts":"{\"engine\":\"coord-desk/1\",...
# The engine is JavaScript, so the honest way to reproduce the app's facts is to
# run the app's own engine. facts.js is the script shown on the cURL tab; fetch
# coords.js next to it once.
import json, pathlib, subprocess, urllib.request
HOST = "https://coord-desk.skillsafe.ai"
if not pathlib.Path("coords.js").exists():
urllib.request.urlretrieve(f"{HOST}/coords.js", "coords.js")
RECORDS = ("#chrom\tchromStart\tchromEnd\tname\n"
"chr1\t11868\t12227\texon1\n"
"1\t69090\t70008\torf1\n")
OPTS = {"target": "region", "contig_style": "ensembl",
"source_basis": "auto", "assembly": "GRCh37"}
def engine(records, opts):
pathlib.Path("in.txt").write_text(records, encoding="utf-8")
out = subprocess.run(["node", "facts.js", "in.txt", opts["target"], opts["contig_style"],
opts["source_basis"], opts["assembly"]],
capture_output=True, check=True).stdout.decode("utf-8")
return json.loads(out) # {"records": clipped text, "facts": the facts STRING}
eng = engine(RECORDS, OPTS)
facts = json.loads(eng["facts"]) # parse it only to inspect it
print(facts["target"]["shift_from_main_basis"], [f["rule"] for f in facts["flags"]])
# start + 1, end unchanged (0-based half-open to 1-based closed) ['mixed-contig-style']
// In Node the engine needs no subprocess: give coords.js a `window` to attach to
// and evaluate it. This is facts.mjs.
import vm from "node:vm";
const HOST = "https://coord-desk.skillsafe.ai";
async function loadEngine() {
globalThis.window = {}; // coords.js attaches here
const src = await fetch(HOST + "/coords.js").then((r) => r.text());
vm.runInThisContext(src); // defines window.CDCoords
return globalThis.window.CDCoords;
}
const RECORDS = [
"#chrom\tchromStart\tchromEnd\tname",
"chr1\t11868\t12227\texon1",
"1\t69090\t70008\torf1",
].join("\n");
const OPTS = { target: "region", contig_style: "ensembl", source_basis: "auto", assembly: "GRCh37" };
const C = await loadEngine();
const clip = C.clipMiddle(RECORDS, C.MAX_TEXT); // 24,000 chars, whole lines
const analysis = C.analyze(clip.text, {
target: OPTS.target, style: OPTS.contig_style, basis: OPTS.source_basis, assembly: OPTS.assembly,
});
const FACTS = JSON.stringify(C.toFacts(analysis, clip));
const CLIPPED = clip.text;
console.log(C.summaryLine(analysis));
// Headered table 2 · 0-based, half-open · assembly unknown · 0 errors, 1 warning
// Go drives the same facts.js as a subprocess. Keep coords.js and facts.js
// beside the binary; coords.js is a static file from https://coord-desk.skillsafe.ai.
const records = "#chrom\tchromStart\tchromEnd\tname\n" +
"chr1\t11868\t12227\texon1\n" +
"1\t69090\t70008\torf1\n"
type engineOut struct {
Records string `json:"records"` // the clipped paste
Facts string `json:"facts"` // the facts as a JSON STRING
}
func engine(records, target, style, basis, assembly string) (engineOut, error) {
var out engineOut
if err := os.WriteFile("in.txt", []byte(records), 0o644); err != nil {
return out, err
}
b, err := exec.Command("node", "facts.js", "in.txt", target, style, basis, assembly).Output()
if err != nil {
return out, fmt.Errorf("the engine failed: %w", err)
}
err = json.Unmarshal(b, &out)
return out, err
}
eng, err := engine(records, "region", "ensembl", "auto", "GRCh37")
if err != nil {
log.Fatal(err)
}
fmt.Println(len(eng.Facts), "characters of facts")
// Java drives the same facts.js as a subprocess. coords.js and facts.js are
// static files kept beside the program.
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
static final String RECORDS = "#chrom\tchromStart\tchromEnd\tname\n"
+ "chr1\t11868\t12227\texon1\n"
+ "1\t69090\t70008\torf1\n";
/** Returns {"records": clipped paste, "facts": the facts STRING}. */
static JsonNode engine(String records, String target, String style, String basis, String assembly) throws Exception {
Files.writeString(Path.of("in.txt"), records, StandardCharsets.UTF_8);
Process p = new ProcessBuilder("node", "facts.js", "in.txt", target, style, basis, assembly).start();
String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
if (p.waitFor() != 0) throw new RuntimeException("the engine failed");
return CoordDesk.MAPPER.readTree(out);
}
JsonNode eng = engine(RECORDS, "region", "ensembl", "auto", "GRCh37");
String FACTS = eng.get("facts").asText(); // a string, sent as a string value
String CLIPPED = eng.get("records").asText();
# Ruby drives the same facts.js as a subprocess. coords.js and facts.js are
# static files kept beside the script.
require "open3"
RECORDS = "#chrom\tchromStart\tchromEnd\tname\n" \
"chr1\t11868\t12227\texon1\n" \
"1\t69090\t70008\torf1\n"
def engine(records, target, style, basis, assembly)
File.write("in.txt", records)
out, err, status = Open3.capture3("node", "facts.js", "in.txt", target, style, basis, assembly)
raise "the engine failed: #{err}" unless status.success?
JSON.parse(out) # {"records" => clipped paste, "facts" => the facts STRING}
end
ENG = engine(RECORDS, "region", "ensembl", "auto", "GRCh37")
puts JSON.parse(ENG["facts"])["flags"].map { |f| "#{f['id']} #{f['rule']}" }
# F-001 mixed-contig-style
<?php
// PHP drives the same facts.js as a subprocess. coords.js and facts.js are
// static files kept beside the script.
$RECORDS = "#chrom\tchromStart\tchromEnd\tname\n"
. "chr1\t11868\t12227\texon1\n"
. "1\t69090\t70008\torf1\n";
function engine(string $records, string $target, string $style, string $basis, string $assembly): array {
file_put_contents("in.txt", $records);
$cmd = "node facts.js in.txt " . implode(" ", array_map("escapeshellarg", [$target, $style, $basis, $assembly]));
$out = shell_exec($cmd);
if (!$out) {
throw new RuntimeException("the engine failed");
}
return json_decode($out, true); // ["records" => clipped paste, "facts" => the facts STRING]
}
$ENG = engine($RECORDS, "region", "ensembl", "auto", "GRCh37");
echo json_decode($ENG["facts"], true)["target"]["shift_start"], PHP_EOL; // 1
// C# drives the same facts.js as a subprocess. coords.js and facts.js are
// static files kept beside the program.
using System.Diagnostics;
const string Records = "#chrom\tchromStart\tchromEnd\tname\n" +
"chr1\t11868\t12227\texon1\n" +
"1\t69090\t70008\torf1\n";
static JsonElement Engine(string records, params string[] opts)
{
File.WriteAllText("in.txt", records);
var psi = new ProcessStartInfo("node") { RedirectStandardOutput = true };
psi.ArgumentList.Add("facts.js");
psi.ArgumentList.Add("in.txt");
foreach (var o in opts) psi.ArgumentList.Add(o);
using var proc = Process.Start(psi)!;
var output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit();
if (proc.ExitCode != 0) throw new Exception("the engine failed");
return JsonDocument.Parse(output).RootElement; // { records, facts (a STRING) }
}
var eng = Engine(Records, "region", "ensembl", "auto", "GRCh37");
var FACTS = eng.GetProperty("facts").GetString()!;
var Clipped = eng.GetProperty("records").GetString()!;
The lazy alternative, and what it costs you. facts is optional, so a
body without it runs. What you get back is a review of the paste alone: coverage: [],
both agrees_with_engine fields false, a notes_on_input that
says nothing was cross-checked, and a shift and assembly call made by reading the text rather than
by the engine's rules. Every safeguard in step 7 then has nothing to compare against. If you cannot
run the engine, at least know that you are buying an opinion, not a check.
/estimate creates no job and charges nothing. Send it the same body
you will run. It returns the reservation and the model binding:
hold_credits is what gets held, min_credits is the balance you must clear
to start at all, model is gpt-5.6-terra, model_alias is
gpt-terra, markup_bps is 1000, and
input_checked and warnings[] report what the server made of the body.
The hold is a reservation, not the price: it prices the full output cap, so the
charged_credits on the settled job is usually far lower. Plan against
hold_credits, account against charged_credits. A guest token can call it.
facts is usually the largest field after records: a long paste with many
flags is many input tokens whether you read them or not, so re-estimate when the paste changes
shape rather than reusing one number for every file.
# Build the body with a JSON encoder, never with string concatenation: the paste has
# tabs and newlines and the facts string has quotes.
python3 - <<'PY' > body.json
import json
eng = json.load(open("engine.json"))
print(json.dumps({
"task": "review",
"records": eng["records"], # the clipped paste
"target": "region", # bed | region | gff | interval_list
"contig_style": "ensembl", # keep | ucsc | ensembl
"source_basis": "auto", # auto | zero | one
"assembly": "GRCh37", # unknown | GRCh37 | GRCh38
"pipeline_hint": "GATK -L targets.list",
"facts": eng["facts"], # the STRING from node facts.js
}))
PY
call estimate "$(cat body.json)"
# {"ok":true,"data":{"hold_credits":...,"min_credits":...,"model":"gpt-5.6-terra",
# "model_alias":"gpt-terra","markup_bps":1000,"input_checked":...,"warnings":[]}}
#
# estimate is FREE. It creates no job and charges nothing. hold_credits is what
# gets RESERVED; charged_credits on the settled job is normally much lower.
# Every value is a string; eng["facts"] is the string from facts.js, NOT a parsed object.
review_input = {
"task": "review",
"records": eng["records"],
"target": OPTS["target"],
"contig_style": OPTS["contig_style"],
"source_basis": OPTS["source_basis"],
"assembly": OPTS["assembly"],
"pipeline_hint": "GATK -L targets.list",
"facts": eng["facts"],
}
est = call("estimate", review_input)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
if me["credits"] < est["min_credits"]:
raise SystemExit("top up first: the balance is below min_credits")
// FACTS is the STRING from the engine step, CLIPPED the clipped paste.
const reviewInput = {
task: "review",
records: CLIPPED,
target: OPTS.target,
contig_style: OPTS.contig_style,
source_basis: OPTS.source_basis,
assembly: OPTS.assembly,
pipeline_hint: "GATK -L targets.list",
facts: FACTS,
};
const est = await call("estimate", reviewInput);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
if (me.credits < est.min_credits) throw new Error("top up first: the balance is below min_credits");
// Every value is a string; eng.Facts is the engine's JSON as a STRING.
reviewInput := map[string]string{
"task": "review",
"records": eng.Records,
"target": "region",
"contig_style": "ensembl",
"source_basis": "auto",
"assembly": "GRCh37",
"pipeline_hint": "GATK -L targets.list",
"facts": eng.Facts,
}
raw, err = call("estimate", reviewInput, nil)
if err != nil {
log.Fatal(err)
}
var est struct {
HoldCredits int `json:"hold_credits"`
MinCredits int `json:"min_credits"`
Model string `json:"model"`
Warnings []any `json:"warnings"`
}
_ = json.Unmarshal(raw, &est)
fmt.Println(est.Model, est.HoldCredits, est.MinCredits, est.Warnings)
Map<String, String> reviewInput = new java.util.LinkedHashMap<>();
reviewInput.put("task", "review");
reviewInput.put("records", CLIPPED);
reviewInput.put("target", "region"); // bed | region | gff | interval_list
reviewInput.put("contig_style", "ensembl"); // keep | ucsc | ensembl
reviewInput.put("source_basis", "auto"); // auto | zero | one
reviewInput.put("assembly", "GRCh37"); // unknown | GRCh37 | GRCh38
reviewInput.put("pipeline_hint", "GATK -L targets.list");
reviewInput.put("facts", FACTS); // a string, serialised once by Jackson
JsonNode est = CoordDesk.call("estimate", reviewInput, null);
System.out.println(est.path("hold_credits").asLong() + " " + est.path("min_credits").asLong()
+ " " + est.path("model").asText());
# ENG["facts"] is the STRING from facts.js.
review_input = {
"task" => "review",
"records" => ENG["records"],
"target" => "region",
"contig_style" => "ensembl",
"source_basis" => "auto",
"assembly" => "GRCh37",
"pipeline_hint" => "GATK -L targets.list",
"facts" => ENG["facts"]
}
est = call("estimate", review_input)
puts "#{est['hold_credits']} #{est['min_credits']} #{est['model']} #{est['warnings']}"
<?php
// $ENG["facts"] is the STRING from facts.js.
$reviewInput = [
"task" => "review",
"records" => $ENG["records"],
"target" => "region",
"contig_style" => "ensembl",
"source_basis" => "auto",
"assembly" => "GRCh37",
"pipeline_hint" => "GATK -L targets.list",
"facts" => $ENG["facts"],
];
$est = call("estimate", $reviewInput);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], PHP_EOL;
var reviewInput = new Dictionary<string, string>
{
["task"] = "review",
["records"] = Clipped,
["target"] = "region", // bed | region | gff | interval_list
["contig_style"] = "ensembl", // keep | ucsc | ensembl
["source_basis"] = "auto", // auto | zero | one
["assembly"] = "GRCh37", // unknown | GRCh37 | GRCh38
["pipeline_hint"] = "GATK -L targets.list",
["facts"] = FACTS, // the STRING from the engine step
};
var est = await CoordDesk.Call("estimate", reviewInput);
Console.WriteLine($"{est.GetProperty("hold_credits")} {est.GetProperty("min_credits")} {est.GetProperty("model")}");
5. Run it, then poll
POST /run takes the same body, reserves hold_credits, starts the job and
returns a job_id at once; the reply arrives when GET /jobs/{id} reports
status: "succeeded". The model's text is the job's output.output string.
Send an Idempotency-Key on every run, derived from the input, the task and an attempt
number: coord-desk:<task>:<first 16 hex of sha256(body)>:a<attempt>.
A retried request with the same key returns the same job instead of billing twice; bump the
attempt suffix only when you mean to run again — after a retry_note, a top-up, or an
edit to any field.
# Same body as the estimate. The key is slug:task:hash:attempt.
KEY="coord-desk:review:$(shasum -a 256 body.json | cut -c1-16):a1"
JOB=$(curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
--data-binary @body.json | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
# Poll until terminal: succeeded | failed | cancelled.
while :; do
STATE=$(curl -sS "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN")
STATUS=$(echo "$STATE" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
case "$STATUS" in succeeded|failed|cancelled) break;; esac
sleep 2
done
echo "$STATE" > state.json
python3 -c 'import json;d=json.load(open("state.json"))["data"];print(d["status"],d.get("charged_credits"),"credits, truncated:",d.get("truncated"));print(d["output"]["output"][:400])'
def idem_key(body, attempt):
digest = hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()[:16]
return f"{SLUG}:{body['task']}:{digest}:a{attempt}"
def run_and_wait(body, attempt=1, poll=2.0):
job = call("run", body, headers={"Idempotency-Key": idem_key(body, attempt)})
while True:
state = call(f"jobs/{job['job_id']}")
if state["status"] in ("succeeded", "failed", "cancelled"):
return state
time.sleep(poll)
state = run_and_wait(review_input)
print(state["status"], state.get("charged_credits"), "credits; truncated:", state.get("truncated"))
raw = state["output"]["output"] # the model's reply: one JSON object as a string
import { createHash } from "node:crypto";
function idemKey(body, attempt) {
const digest = createHash("sha256").update(JSON.stringify(body)).digest("hex").slice(0, 16);
return `${SLUG}:${body.task}:${digest}:a${attempt}`;
}
async function runAndWait(body, attempt = 1, pollMs = 2000) {
const job = await call("run", body, { "Idempotency-Key": idemKey(body, attempt) });
for (;;) {
const state = await call(`jobs/${job.job_id}`);
if (["succeeded", "failed", "cancelled"].includes(state.status)) return state;
await new Promise((r) => setTimeout(r, pollMs));
}
}
const state = await runAndWait(reviewInput);
console.log(state.status, state.charged_credits, "credits; truncated:", state.truncated);
const raw = state.output.output; // the model's reply as a string
type jobState struct {
JobID string `json:"job_id"`
Status string `json:"status"`
ChargedCredits int `json:"charged_credits"`
Truncated bool `json:"truncated"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
func idemKey(body map[string]string, attempt int) string {
enc, _ := json.Marshal(body) // Go sorts map keys, so the hash is stable
sum := sha256.Sum256(enc)
return fmt.Sprintf("%s:%s:%x:a%d", slug, body["task"], sum[:8], attempt)
}
func runAndWait(body map[string]string, attempt int) (jobState, error) {
var st jobState
raw, err := call("run", body, map[string]string{"Idempotency-Key": idemKey(body, attempt)})
if err != nil {
return st, err
}
var job jobState
_ = json.Unmarshal(raw, &job)
for {
raw, err := call("jobs/"+job.JobID, nil, nil)
if err != nil {
return st, err
}
_ = json.Unmarshal(raw, &st)
switch st.Status {
case "succeeded", "failed", "cancelled":
return st, nil
}
time.Sleep(2 * time.Second)
}
}
// state, err := runAndWait(reviewInput, 1)
// replyText := state.Output.Output
import java.security.MessageDigest;
import java.util.HexFormat;
static String idemKey(Map<String, String> body, int attempt) throws Exception {
byte[] enc = CoordDesk.MAPPER.writeValueAsBytes(body);
String digest = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(enc)).substring(0, 16);
return CoordDesk.SLUG + ":" + body.get("task") + ":" + digest + ":a" + attempt;
}
static JsonNode runAndWait(Map<String, String> body, int attempt) throws Exception {
JsonNode job = CoordDesk.call("run", body, Map.of("Idempotency-Key", idemKey(body, attempt)));
while (true) {
JsonNode state = CoordDesk.call("jobs/" + job.get("job_id").asText(), null, null);
String status = state.get("status").asText();
if (status.equals("succeeded") || status.equals("failed") || status.equals("cancelled")) return state;
Thread.sleep(2000);
}
}
// JsonNode state = runAndWait(reviewInput, 1);
// String raw = state.get("output").get("output").asText();
def idem_key(body, attempt)
digest = Digest::SHA256.hexdigest(JSON.generate(body))[0, 16]
"#{SLUG}:#{body['task']}:#{digest}:a#{attempt}"
end
def run_and_wait(body, attempt = 1, poll = 2)
job = call("run", body, { "Idempotency-Key" => idem_key(body, attempt) })
loop do
state = call("jobs/#{job['job_id']}")
return state if %w[succeeded failed cancelled].include?(state["status"])
sleep poll
end
end
state = run_and_wait(review_input)
puts "#{state['status']} #{state['charged_credits']} credits; truncated: #{state['truncated']}"
raw = state["output"]["output"]
<?php
function idemKey(array $body, int $attempt): string {
$digest = substr(hash("sha256", json_encode($body)), 0, 16);
return SLUG . ":" . $body["task"] . ":" . $digest . ":a" . $attempt;
}
function runAndWait(array $body, int $attempt = 1, int $poll = 2): array {
$job = call("run", $body, ["Idempotency-Key: " . idemKey($body, $attempt)]);
while (true) {
$state = call("jobs/" . $job["job_id"]);
if (in_array($state["status"], ["succeeded", "failed", "cancelled"], true)) return $state;
sleep($poll);
}
}
$state = runAndWait($reviewInput);
echo $state["status"], " ", $state["charged_credits"] ?? "", " credits; truncated: ",
var_export($state["truncated"] ?? false, true), "\n";
$raw = $state["output"]["output"];
static string IdemKey(Dictionary<string, string> body, int attempt)
{
var enc = JsonSerializer.SerializeToUtf8Bytes(body);
var digest = Convert.ToHexString(SHA256.HashData(enc)).ToLowerInvariant()[..16];
return $"{CoordDesk.Slug}:{body["task"]}:{digest}:a{attempt}";
}
static async Task<JsonElement> RunAndWait(Dictionary<string, string> body, int attempt = 1)
{
var job = await CoordDesk.Call("run", body, new() { ["Idempotency-Key"] = IdemKey(body, attempt) });
while (true)
{
var state = await CoordDesk.Call($"jobs/{job.GetProperty("job_id").GetString()}");
var status = state.GetProperty("status").GetString();
if (status is "succeeded" or "failed" or "cancelled") return state;
await Task.Delay(2000);
}
}
// var state = await RunAndWait(reviewInput);
// var raw = state.GetProperty("output").GetProperty("output").GetString();
6. Or stream it
POST /run-stream takes the same body and the same kind of key and answers with
server-sent events. From a script you see event: delta frames carrying pieces of the
reply and one final event: done with the finished job — its status,
charged_credits, truncated and the full output.output. From a
browser page the platform sends only tick heartbeats and the done event,
which is why the app's progress card advances on elapsed time between the real signals.
Concatenate the deltas; parse only when done arrives, and prefer the
done event's output.output over your own concatenation when both exist.
curl -sN -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Idempotency-Key: coord-desk:review:$(shasum -a 256 body.json | cut -c1-16):a1" \
--data-binary @body.json
# event: job data: {"job_id":"job_..."}
# event: delta data: {"text":"{\"lane\":\"review\",\"title\":\"Exon targets, UCSC table to GRCh37 region list\","}
# event: delta data: {"text":"\"summary\":\"..."}
# event: done data: {"status":"succeeded","charged_credits":...,"truncated":false,"output":{"output":"{...}"}}
def run_stream(body, attempt=1):
req = urllib.request.Request(f"{BASE}/run-stream", data=json.dumps(body).encode(), method="POST")
for k, v in {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json",
"Accept": "text/event-stream", "Idempotency-Key": idem_key(body, attempt)}.items():
req.add_header(k, v)
event, buf, done = None, [], None
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().rstrip("\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data = json.loads(line[5:])
if event == "delta":
buf.append(data.get("text", ""))
elif event == "done":
done = data
raw = (done or {}).get("output", {}).get("output") or "".join(buf)
return done, raw
done, raw = run_stream(review_input)
print(done["status"], done.get("charged_credits"), "truncated:", done.get("truncated"))
async function runStream(body, attempt = 1) {
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json",
Accept: "text/event-stream", "Idempotency-Key": idemKey(body, attempt) },
body: JSON.stringify(body),
});
const reader = res.body.getReader(), dec = new TextDecoder();
let pending = "", event = null, buf = "", done = null;
for (;;) {
const { value, done: end } = await reader.read();
if (end) break;
pending += dec.decode(value, { stream: true });
let i;
while ((i = pending.indexOf("\n")) !== -1) {
const line = pending.slice(0, i).replace(/\r$/, ""); pending = pending.slice(i + 1);
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) {
const data = JSON.parse(line.slice(5));
if (event === "delta") buf += data.text || ""; // a STRING, not an object
else if (event === "done") done = data;
}
}
}
return { done, raw: (done && done.output && done.output.output) || buf };
}
func runStream(body map[string]string, attempt int) (map[string]any, string, error) {
enc, _ := json.Marshal(body)
req, _ := http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(enc))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Idempotency-Key", idemKey(body, attempt))
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, "", err
}
defer res.Body.Close()
var event string
var buf strings.Builder
var done map[string]any
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<24)
for sc.Scan() {
line := strings.TrimRight(sc.Text(), "\r")
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
var data map[string]any
if json.Unmarshal([]byte(line[5:]), &data) != nil {
continue
}
if event == "delta" {
if t, ok := data["text"].(string); ok {
buf.WriteString(t)
}
} else if event == "done" {
done = data
}
}
}
raw := buf.String()
if out, ok := done["output"].(map[string]any); ok {
if s, ok := out["output"].(string); ok {
raw = s
}
}
return done, raw, nil
}
static String[] runStream(Map<String, String> body, int attempt) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(CoordDesk.BASE + "/run-stream"))
.header("Authorization", "Bearer " + CoordDesk.TOKEN)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("Idempotency-Key", idemKey(body, attempt))
.POST(HttpRequest.BodyPublishers.ofString(CoordDesk.MAPPER.writeValueAsString(body))).build();
HttpResponse<java.io.InputStream> res = CoordDesk.HTTP.send(req, HttpResponse.BodyHandlers.ofInputStream());
String event = null; JsonNode done = null; StringBuilder buf = new StringBuilder();
try (var in = new java.io.BufferedReader(new java.io.InputStreamReader(res.body()))) {
String line;
while ((line = in.readLine()) != null) {
if (line.startsWith("event:")) event = line.substring(6).trim();
else if (line.startsWith("data:")) {
JsonNode data = CoordDesk.MAPPER.readTree(line.substring(5));
if ("delta".equals(event)) buf.append(data.path("text").asText(""));
else if ("done".equals(event)) done = data;
}
}
}
String raw = done != null && done.path("output").has("output")
? done.get("output").get("output").asText() : buf.toString();
return new String[] { done == null ? null : done.toString(), raw };
}
def run_stream(body, attempt = 1)
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{TOKEN}", "Content-Type" => "application/json",
"Accept" => "text/event-stream", "Idempotency-Key" => idem_key(body, attempt))
req.body = JSON.generate(body)
event = nil; buf = +""; done = nil; pending = +""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
pending << chunk
while (i = pending.index("\n"))
line = pending.slice!(0, i + 1).chomp
if line.start_with?("event:") then event = line[6..].strip
elsif line.start_with?("data:")
data = JSON.parse(line[5..])
if event == "delta" then buf << (data["text"] || "")
elsif event == "done" then done = data end
end
end
end
end
end
[done, done&.dig("output", "output") || buf]
end
<?php
function runStream(array $body, int $attempt = 1): array {
$ch = curl_init(BASE . "/run-stream");
$event = null; $buf = ""; $done = null; $pending = "";
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json",
"Accept: text/event-stream", "Idempotency-Key: " . idemKey($body, $attempt)],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$buf, &$done, &$pending) {
$pending .= $chunk;
while (($i = strpos($pending, "\n")) !== false) {
$line = rtrim(substr($pending, 0, $i), "\r"); $pending = substr($pending, $i + 1);
if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
elseif (str_starts_with($line, "data:")) {
$data = json_decode(substr($line, 5), true);
if ($event === "delta") $buf .= $data["text"] ?? "";
elseif ($event === "done") $done = $data;
}
}
return strlen($chunk);
},
]);
curl_exec($ch); curl_close($ch);
return [$done, $done["output"]["output"] ?? $buf];
}
static async Task<(JsonElement? done, string raw)> RunStream(Dictionary<string, string> body, int attempt = 1)
{
var enc = JsonSerializer.SerializeToUtf8Bytes(body);
using var req = new HttpRequestMessage(HttpMethod.Post, $"{CoordDesk.Base}/run-stream");
req.Headers.Authorization = new("Bearer", CoordDesk.Token);
req.Headers.Accept.ParseAdd("text/event-stream");
req.Headers.Add("Idempotency-Key", IdemKey(body, attempt));
req.Content = new ByteArrayContent(enc) { Headers = { ContentType = new("application/json") } };
using var res = await CoordDesk.Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string? ev = null; var buf = new StringBuilder(); JsonElement? done = null;
while (await reader.ReadLineAsync() is { } line)
{
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:"))
{
var data = JsonDocument.Parse(line[5..]).RootElement;
if (ev == "delta") buf.Append(data.TryGetProperty("text", out var t) ? t.GetString() : "");
else if (ev == "done") done = data.Clone();
}
}
var raw = done is { } d && d.TryGetProperty("output", out var o) && o.TryGetProperty("output", out var s)
? s.GetString() ?? buf.ToString() : buf.ToString();
return (done, raw);
}
7. Parse the result
The reply is one JSON object, as a string, in output.output — the polled job's, or the
done event's. Unwrap it twice: once for the envelope, once for the model's text. The
prompt forbids code fences, but parse defensively the way the page does: strip any
``` fence, take everything from the first { to the last
}, and JSON.parse that. Then check the reply against the
facts you sent before you trust a single field. Four checks catch nearly everything:
coverage has exactly the engine's flag ids, in the engine's order; the stated shift
equals facts.target.shift_start / shift_end unless
source.agrees_with_engine is false (then source.reasoning
must say which evidence proves the engine wrong); issues and steps ids
run in sequence; and every enum is one of the listed values.
# Unwrap twice, then check coverage, shift and verdict with python.
python3 - state.json body.json <<'PY'
import json, re, sys
state = json.load(open(sys.argv[1]))["data"]
facts = json.loads(json.load(open(sys.argv[2]))["facts"])
t = re.sub(r"^```[a-zA-Z]*\s*|```\s*$", "", state["output"]["output"].strip())
reply = json.loads(t[t.index("{"):t.rindex("}") + 1]) # the model's single JSON object
print("coverage matches flags:", [c["id"] for c in reply["coverage"]] == [f["id"] for f in facts["flags"]])
src = reply["source"]
print("shift:", src["shift_start"], src["shift_end"], "| engine:",
facts["target"]["shift_start"], facts["target"]["shift_end"], "| agrees:", src["agrees_with_engine"])
print("verdict:", reply["verdict"], "-", reply["verdict_reason"])
PY
import re
VERDICTS = {"safe_to_convert", "convert_with_fixes", "stop_and_check"}
SEVERITIES = {"blocker", "serious", "minor"}
STATUSES = {"confirmed", "downgraded", "dismissed", "merged"}
def parse_reply(raw):
t = re.sub(r"^```[a-zA-Z]*\s*|```\s*$", "", raw.strip())
return json.loads(t[t.index("{"):t.rindex("}") + 1])
def check_reply(reply, body):
problems = []
facts = json.loads(body["facts"]) if body.get("facts") else None
if reply.get("lane") != "review":
problems.append(f"lane is {reply.get('lane')!r}, not 'review'")
if reply.get("verdict") not in VERDICTS:
problems.append(f"unknown verdict {reply.get('verdict')!r}")
if facts:
want = [f["id"] for f in facts["flags"]]
got = [c.get("id") for c in reply.get("coverage", [])]
if got != want:
problems.append(f"coverage {got} does not match the engine's flags {want}")
if any(c.get("status") not in STATUSES for c in reply.get("coverage", [])):
problems.append("a coverage status is not one of the listed values")
src, tgt = reply.get("source", {}), facts["target"]
if src.get("agrees_with_engine") and (src.get("shift_start"), src.get("shift_end")) != (tgt["shift_start"], tgt["shift_end"]):
problems.append("the stated shift differs from the engine's but claims to agree")
for key, prefix in (("issues", "I"), ("steps", "S")):
ids = [x.get("id") for x in reply.get(key, [])]
if ids != [f"{prefix}-{i:03d}" for i in range(1, len(ids) + 1)]:
problems.append(f"{key} ids are not {prefix}-001, {prefix}-002, ... in order")
if any(i.get("severity") not in SEVERITIES for i in reply.get("issues", [])):
problems.append("an issue severity is not blocker, serious or minor")
if not reply.get("steps") or not reply.get("checks_after"):
problems.append("steps or checks_after is empty")
return problems
reply = parse_reply(raw)
problems = check_reply(reply, review_input)
print(reply["title"]); print(reply["verdict"]); print(problems or "clean against the engine")
const VERDICTS = ["safe_to_convert", "convert_with_fixes", "stop_and_check"];
const SEVERITIES = ["blocker", "serious", "minor"];
function parseReply(raw) {
const t = raw.trim().replace(/^```[a-z]*\s*/i, "").replace(/```\s*$/, "");
return JSON.parse(t.slice(t.indexOf("{"), t.lastIndexOf("}") + 1));
}
function checkReply(reply, body) {
const problems = [];
const facts = body.facts ? JSON.parse(body.facts) : null;
if (reply.lane !== "review") problems.push(`lane is ${reply.lane}, not review`);
if (!VERDICTS.includes(reply.verdict)) problems.push(`unknown verdict ${reply.verdict}`);
if (facts) {
const want = facts.flags.map((f) => f.id).join(), got = (reply.coverage || []).map((c) => c.id).join();
if (got !== want) problems.push(`coverage [${got}] does not match the engine's flags [${want}]`);
const s = reply.source || {}, t = facts.target;
if (s.agrees_with_engine && (s.shift_start !== t.shift_start || s.shift_end !== t.shift_end)) {
problems.push("the stated shift differs from the engine's but claims to agree");
}
}
for (const [key, p] of [["issues", "I"], ["steps", "S"]]) {
(reply[key] || []).forEach((x, i) => {
if (x.id !== `${p}-${String(i + 1).padStart(3, "0")}`) problems.push(`${key}[${i}] has id ${x.id}`);
});
}
if ((reply.issues || []).some((i) => !SEVERITIES.includes(i.severity))) problems.push("an issue severity is not listed");
if (!(reply.steps || []).length || !(reply.checks_after || []).length) problems.push("steps or checks_after is empty");
return problems;
}
const reply = parseReply(raw);
const problems = checkReply(reply, reviewInput);
console.log(reply.title, reply.verdict, problems.length ? problems : "clean against the engine");
type Reply struct {
Lane string `json:"lane"`
Title string `json:"title"`
Verdict string `json:"verdict"`
VerdictReason string `json:"verdict_reason"`
Source struct {
ShiftStart int `json:"shift_start"`
ShiftEnd int `json:"shift_end"`
AgreesWithEngine bool `json:"agrees_with_engine"`
} `json:"source"`
Coverage []struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"coverage"`
Issues []struct {
ID string `json:"id"`
Severity string `json:"severity"`
Lines []int `json:"lines"`
What string `json:"what"`
Fix string `json:"fix"`
} `json:"issues"`
Steps []struct {
ID string `json:"id"`
Command string `json:"command"`
} `json:"steps"`
ChecksAfter []string `json:"checks_after"`
}
type Facts struct {
Flags []struct{ ID string `json:"id"` } `json:"flags"`
Target struct {
ShiftStart int `json:"shift_start"`
ShiftEnd int `json:"shift_end"`
} `json:"target"`
}
func parseReply(raw string) (Reply, error) {
var r Reply
t := strings.TrimSpace(raw)
i, j := strings.Index(t, "{"), strings.LastIndex(t, "}")
if i < 0 || j < i {
return r, fmt.Errorf("no JSON object in the reply")
}
return r, json.Unmarshal([]byte(t[i:j+1]), &r)
}
func checkReply(r Reply, body map[string]string) []string {
var problems []string
switch r.Verdict {
case "safe_to_convert", "convert_with_fixes", "stop_and_check":
default:
problems = append(problems, "unknown verdict "+r.Verdict)
}
var f Facts
if json.Unmarshal([]byte(body["facts"]), &f) == nil {
if len(r.Coverage) != len(f.Flags) {
problems = append(problems, "coverage does not have one entry per engine flag")
} else {
for k := range f.Flags {
if r.Coverage[k].ID != f.Flags[k].ID {
problems = append(problems, "coverage out of order at "+f.Flags[k].ID)
}
}
}
if r.Source.AgreesWithEngine && (r.Source.ShiftStart != f.Target.ShiftStart || r.Source.ShiftEnd != f.Target.ShiftEnd) {
problems = append(problems, "the stated shift differs from the engine's but claims to agree")
}
}
for k, is := range r.Issues {
if is.ID != fmt.Sprintf("I-%03d", k+1) {
problems = append(problems, "issue ids out of sequence at "+is.ID)
}
}
for k, st := range r.Steps {
if st.ID != fmt.Sprintf("S-%03d", k+1) {
problems = append(problems, "step ids out of sequence at "+st.ID)
}
}
return problems
}
static JsonNode parseReply(String raw) throws Exception {
String t = raw.trim();
return CoordDesk.MAPPER.readTree(t.substring(t.indexOf('{'), t.lastIndexOf('}') + 1));
}
static List<String> checkReply(JsonNode reply, Map<String, String> body) throws Exception {
List<String> problems = new ArrayList<>();
if (!Set.of("safe_to_convert", "convert_with_fixes", "stop_and_check").contains(reply.path("verdict").asText()))
problems.add("unknown verdict " + reply.path("verdict").asText());
if (body.get("facts") != null) {
JsonNode facts = CoordDesk.MAPPER.readTree(body.get("facts"));
List<String> want = new ArrayList<>(), got = new ArrayList<>();
facts.get("flags").forEach(f -> want.add(f.get("id").asText()));
reply.path("coverage").forEach(c -> got.add(c.path("id").asText()));
if (!want.equals(got)) problems.add("coverage " + got + " does not match the engine's flags " + want);
JsonNode s = reply.path("source"), t = facts.get("target");
if (s.path("agrees_with_engine").asBoolean(false)
&& (s.path("shift_start").asInt() != t.path("shift_start").asInt() || s.path("shift_end").asInt() != t.path("shift_end").asInt()))
problems.add("the stated shift differs from the engine's but claims to agree");
}
int n = 0;
for (JsonNode is : reply.path("issues")) if (!is.path("id").asText().equals(String.format("I-%03d", ++n))) problems.add("issue ids out of sequence");
n = 0;
for (JsonNode st : reply.path("steps")) if (!st.path("id").asText().equals(String.format("S-%03d", ++n))) problems.add("step ids out of sequence");
return problems;
}
VERDICTS = %w[safe_to_convert convert_with_fixes stop_and_check].freeze
def parse_reply(raw)
t = raw.strip.sub(/\A```[a-zA-Z]*\s*/, "").sub(/```\s*\z/, "")
JSON.parse(t[t.index("{")..t.rindex("}")])
end
def check_reply(reply, body)
problems = []
problems << "unknown verdict #{reply['verdict'].inspect}" unless VERDICTS.include?(reply["verdict"])
if body["facts"]
facts = JSON.parse(body["facts"])
want = facts["flags"].map { |f| f["id"] }
got = (reply["coverage"] || []).map { |c| c["id"] }
problems << "coverage #{got} does not match the engine's flags #{want}" unless got == want
s, t = reply["source"] || {}, facts["target"]
if s["agrees_with_engine"] && [s["shift_start"], s["shift_end"]] != [t["shift_start"], t["shift_end"]]
problems << "the stated shift differs from the engine's but claims to agree"
end
end
{ "issues" => "I", "steps" => "S" }.each do |key, p|
ids = (reply[key] || []).map { |x| x["id"] }
problems << "#{key} ids out of sequence" unless ids == (1..ids.size).map { |i| format("%s-%03d", p, i) }
end
problems
end
reply = parse_reply(raw)
problems = check_reply(reply, review_input)
puts reply["title"], reply["verdict"], (problems.empty? ? "clean against the engine" : problems)
<?php
function parseReply(string $raw): array {
$t = preg_replace(['/^```[a-zA-Z]*\s*/', '/```\s*$/'], "", trim($raw));
return json_decode(substr($t, strpos($t, "{"), strrpos($t, "}") - strpos($t, "{") + 1), true);
}
function checkReply(array $reply, array $body): array {
$problems = [];
if (!in_array($reply["verdict"] ?? null, ["safe_to_convert", "convert_with_fixes", "stop_and_check"], true))
$problems[] = "unknown verdict " . var_export($reply["verdict"] ?? null, true);
if (!empty($body["facts"])) {
$facts = json_decode($body["facts"], true);
$want = array_column($facts["flags"], "id");
$got = array_column($reply["coverage"] ?? [], "id");
if ($want !== $got) $problems[] = "coverage does not match the engine's flags";
$s = $reply["source"] ?? []; $t = $facts["target"];
if (!empty($s["agrees_with_engine"]) && ([$s["shift_start"] ?? null, $s["shift_end"] ?? null] !== [$t["shift_start"], $t["shift_end"]]))
$problems[] = "the stated shift differs from the engine's but claims to agree";
}
foreach (["issues" => "I", "steps" => "S"] as $key => $p) {
foreach (array_values($reply[$key] ?? []) as $i => $x) {
if (($x["id"] ?? "") !== sprintf("%s-%03d", $p, $i + 1)) { $problems[] = "$key ids out of sequence"; break; }
}
}
return $problems;
}
$reply = parseReply($raw);
$problems = checkReply($reply, $reviewInput);
echo $reply["title"], "\n", $reply["verdict"], "\n", $problems ? implode("\n", $problems) : "clean against the engine", "\n";
static JsonElement ParseReply(string raw)
{
var t = raw.Trim();
return JsonDocument.Parse(t[t.IndexOf('{')..(t.LastIndexOf('}') + 1)]).RootElement;
}
static List<string> CheckReply(JsonElement reply, Dictionary<string, string> body)
{
var problems = new List<string>();
var verdict = reply.TryGetProperty("verdict", out var v) ? v.GetString() : null;
if (verdict is not ("safe_to_convert" or "convert_with_fixes" or "stop_and_check"))
problems.Add($"unknown verdict {verdict}");
if (body.TryGetValue("facts", out var factsText) && !string.IsNullOrEmpty(factsText))
{
var facts = JsonDocument.Parse(factsText).RootElement;
var want = facts.GetProperty("flags").EnumerateArray().Select(f => f.GetProperty("id").GetString()).ToList();
var got = reply.TryGetProperty("coverage", out var c)
? c.EnumerateArray().Select(x => x.GetProperty("id").GetString()).ToList() : new();
if (!want.SequenceEqual(got)) problems.Add("coverage does not match the engine's flags");
var s = reply.GetProperty("source"); var t = facts.GetProperty("target");
if (s.GetProperty("agrees_with_engine").GetBoolean()
&& (s.GetProperty("shift_start").GetInt32() != t.GetProperty("shift_start").GetInt32()
|| s.GetProperty("shift_end").GetInt32() != t.GetProperty("shift_end").GetInt32()))
problems.Add("the stated shift differs from the engine's but claims to agree");
}
foreach (var (key, p) in new[] { ("issues", "I"), ("steps", "S") })
{
var n = 0;
foreach (var x in reply.GetProperty(key).EnumerateArray())
if (x.GetProperty("id").GetString() != $"{p}-{++n:000}") { problems.Add($"{key} ids out of sequence"); break; }
}
return problems;
}
The output contract
The reply is ONE JSON object with exactly these keys — no prose around it, no fences. The keys and enum values are exact; a script should treat an unknown value as a defect, not a feature.
{
"lane": "review",
"title": string, // names the data and the conversion
"summary": string, // two to four sentences
"verdict": "safe_to_convert" | "convert_with_fixes" | "stop_and_check",
"verdict_reason": string, // one sentence
"source": {
"format": "bed" | "table" | "vcf" | "gff" | "sam" | "interval_list" | "region" | "mixed",
"basis": "0-based half-open" | "1-based closed" | "mixed" | "unclear",
"shift_start": -1 | 0 | 1, // integer, source basis to target
"shift_end": -1 | 0 | 1,
"agrees_with_engine": boolean,
"reasoning": string
},
"assembly": {
"call": "GRCh37" | "GRCh38" | "T2T-CHM13" | "unknown" | "conflict",
"agrees_with_engine": boolean,
"reasoning": string,
"how_to_confirm": string // one sentence
},
"coverage": [
{ "id": "F-001", "status": "confirmed" | "downgraded" | "dismissed" | "merged",
"ref": "I-001" | "", "note": string }
],
"issues": [
{ "id": "I-001", "severity": "blocker" | "serious" | "minor", "lines": [integer],
"what": string, "why_it_matters": string, "fix": string }
],
"steps": [
{ "id": "S-001",
"tool": "awk" | "bedtools" | "bcftools" | "samtools" | "picard" | "gatk" | "crossmap" | "liftover" | "tabix" | "sort" | "sed" | "other",
"purpose": string, "command": string }
],
"checks_after": [string],
"notes_on_input": string // "" or what was odd about the input
}
| key | rules |
|---|---|
verdict | safe_to_convert: no error-severity flag is confirmed and the basis and assembly are clear. convert_with_fixes: the conversion is sound once the listed issues are fixed. stop_and_check: something cannot be resolved from the paste — an assembly conflict, an off-by-one pair, positions past a chromosome end, a chrM/MT rename on hg19, a contradicted override — and verdict_reason says who or what must answer it. |
source | format is what the source really is; basis uses the reply's own spellings ("0-based half-open", without the comma the facts use). The shift is two integers from the source basis to the target — BED to region is +1/0, a 1-based table to BED is -1/0, same basis is 0/0 — and must equal facts.target.shift_start/shift_end unless agrees_with_engine is false and reasoning names the flag or evidence that proves the engine's basis wrong. |
assembly | Never claims an assembly the evidence does not support; unknown is an honest answer. how_to_confirm is one sentence: a header line, a known position, asking the source. |
coverage | Exactly one entry per flag in facts.flags, same ids, same order, no other ids. confirmed = real and it matters here; downgraded = real but minor for this pipeline; dismissed = not a problem here, with the reason; merged = same root cause as another flag, named in note. A confirmed or downgraded flag that needs action points at its issue via ref; otherwise ref is "". Empty when no facts were sent. |
issues | Ids I-001, I-002, … in order, blockers first. lines are integer line numbers of the paste. May be empty only when there is genuinely nothing to fix. |
steps | Ids S-001, … in order; always at least the conversion itself. One command or one pipeline per step, with placeholder file names (in.bed, out.bed, in.vcf.gz, ref.fa, chain.over.chain.gz); fixes come before conversion (rename contigs, split multi-allelics, normalise against the reference, then convert, then sort and index). The shift in any awk step must be the shift stated in source. No step states a liftover result. |
checks_after | At least one concrete check, e.g. "the first record should read chr1:12,011-12,227" or "bcftools norm -c e reports zero REF mismatches". |
notes_on_input | "", or what was odd about the input itself: clipped, task missing, unreadable lines, no facts supplied. |
Every string is plain text — no Markdown, no HTML — so a command in steps[].command can
be copied as it stands. It is still a suggestion from a model: read it before you run it, and run it
on copies.
8. Use it as a gate
A coordinate file on its way into a pipeline can be checked on every change. The engine alone (no
credits) already fails the obvious cases — any error-severity flag is worth stopping
on. The review (metered) adds the judgement: fail the CI step when the verdict is
stop_and_check or any issue has severity blocker, and also when the run did
not complete, was truncated, or disagrees with the engine.
#!/bin/sh
# CI gate: run the review over targets.bed and fail on stop_and_check or a blocker.
set -e
node facts.js targets.bed region ensembl auto GRCh37 > engine.json
python3 - <<'PY' > body.json
import json
eng = json.load(open("engine.json"))
print(json.dumps({"task": "review", "records": eng["records"], "target": "region",
"contig_style": "ensembl", "source_basis": "auto", "assembly": "GRCh37",
"pipeline_hint": "GATK -L targets.list", "facts": eng["facts"]}))
PY
# ... run and poll exactly as in step 5, leaving the finished job in state.json ...
python3 - <<'PY'
import json, re, sys
d = json.load(open("state.json"))["data"]
if d["status"] != "succeeded" or d.get("truncated"):
sys.exit(f"review did not complete: {d['status']} truncated={d.get('truncated')}")
t = re.sub(r"^```[a-zA-Z]*\s*|```\s*$", "", d["output"]["output"].strip())
r = json.loads(t[t.index("{"):t.rindex("}") + 1])
blockers = [i for i in r["issues"] if i["severity"] == "blocker"]
for i in blockers:
print(f"{i['id']} lines {i['lines']}: {i['what']} -- fix: {i['fix']}", file=sys.stderr)
print("verdict:", r["verdict"], "-", r["verdict_reason"])
if r["verdict"] == "stop_and_check" or blockers:
sys.exit(2)
PY
# Metered gate: fail the build on stop_and_check or any blocker issue.
import sys
OPTS = {"target": "region", "contig_style": "ensembl", "source_basis": "auto", "assembly": "GRCh37"}
eng = engine(open("targets.bed").read(), OPTS) # the step-4 helper
body = {"task": "review", "records": eng["records"], **OPTS,
"pipeline_hint": "GATK -L targets.list", "facts": eng["facts"]}
state = run_and_wait(body)
if state["status"] != "succeeded" or state.get("truncated"):
sys.exit(f"review did not complete: {state['status']} truncated={state.get('truncated')}")
reply = parse_reply(state["output"]["output"])
problems = check_reply(reply, body)
if problems:
sys.exit("reply disagrees with the engine: " + "; ".join(problems))
blockers = [i for i in reply["issues"] if i["severity"] == "blocker"]
for i in blockers:
print(f" {i['id']} lines {i['lines']}: {i['what']}\n fix: {i['fix']}", file=sys.stderr)
print(f"verdict: {reply['verdict']} - {reply['verdict_reason']}")
if reply["verdict"] == "stop_and_check" or blockers:
sys.exit(2)
// Metered gate for a CI step (Node 20+). C is the engine from step 4.
import { readFileSync } from "node:fs";
const text = readFileSync("targets.bed", "utf8");
const opts = { target: "region", contig_style: "ensembl", source_basis: "auto", assembly: "GRCh37" };
const clip = C.clipMiddle(text, C.MAX_TEXT);
const facts = JSON.stringify(C.toFacts(C.analyze(clip.text, {
target: opts.target, style: opts.contig_style, basis: opts.source_basis, assembly: opts.assembly,
}), clip));
const body = { task: "review", records: clip.text, ...opts, pipeline_hint: "GATK -L targets.list", facts };
const state = await runAndWait(body);
if (state.status !== "succeeded" || state.truncated) {
console.error("review did not complete", state.status, state.truncated); process.exit(1);
}
const reply = parseReply(state.output.output);
const problems = checkReply(reply, body);
if (problems.length) { console.error("reply disagrees with the engine:", problems); process.exit(1); }
const blockers = reply.issues.filter((i) => i.severity === "blocker");
for (const i of blockers) console.error(` ${i.id} lines ${i.lines}: ${i.what}\n fix: ${i.fix}`);
console.log(`verdict: ${reply.verdict} - ${reply.verdict_reason}`);
if (reply.verdict === "stop_and_check" || blockers.length) process.exit(2);
func main() {
text, _ := os.ReadFile("targets.bed")
eng, err := engine(string(text), "region", "ensembl", "auto", "GRCh37") // the step-4 helper
if err != nil {
log.Fatal(err)
}
body := map[string]string{
"task": "review", "records": eng.Records, "target": "region", "contig_style": "ensembl",
"source_basis": "auto", "assembly": "GRCh37", "pipeline_hint": "GATK -L targets.list", "facts": eng.Facts,
}
state, err := runAndWait(body, 1)
if err != nil || state.Status != "succeeded" || state.Truncated {
log.Fatalf("review did not complete: %s truncated=%v %v", state.Status, state.Truncated, err)
}
reply, err := parseReply(state.Output.Output)
if err != nil {
log.Fatal(err)
}
if p := checkReply(reply, body); len(p) > 0 {
log.Fatalf("reply disagrees with the engine: %v", p)
}
blockers := 0
for _, is := range reply.Issues {
if is.Severity == "blocker" {
blockers++
fmt.Fprintf(os.Stderr, " %s lines %v: %s\n fix: %s\n", is.ID, is.Lines, is.What, is.Fix)
}
}
fmt.Printf("verdict: %s - %s\n", reply.Verdict, reply.VerdictReason)
if reply.Verdict == "stop_and_check" || blockers > 0 {
os.Exit(2)
}
}
public static void main(String[] args) throws Exception {
JsonNode eng = engine(Files.readString(Path.of("targets.bed")), "region", "ensembl", "auto", "GRCh37");
Map<String, String> body = new java.util.LinkedHashMap<>();
body.put("task", "review");
body.put("records", eng.get("records").asText());
body.put("target", "region");
body.put("contig_style", "ensembl");
body.put("source_basis", "auto");
body.put("assembly", "GRCh37");
body.put("pipeline_hint", "GATK -L targets.list");
body.put("facts", eng.get("facts").asText());
JsonNode state = runAndWait(body, 1);
if (!state.get("status").asText().equals("succeeded") || state.path("truncated").asBoolean(false)) {
System.err.println("review did not complete: " + state.get("status")); System.exit(1);
}
JsonNode reply = parseReply(state.get("output").get("output").asText());
List<String> problems = checkReply(reply, body);
if (!problems.isEmpty()) { System.err.println("reply disagrees with the engine: " + problems); System.exit(1); }
int blockers = 0;
for (JsonNode is : reply.path("issues")) {
if (is.path("severity").asText().equals("blocker")) {
blockers++;
System.err.printf(" %s lines %s: %s%n fix: %s%n", is.path("id").asText(), is.path("lines"), is.path("what").asText(), is.path("fix").asText());
}
}
System.out.println("verdict: " + reply.path("verdict").asText() + " - " + reply.path("verdict_reason").asText());
if (reply.path("verdict").asText().equals("stop_and_check") || blockers > 0) System.exit(2);
}
eng = engine(File.read("targets.bed"), "region", "ensembl", "auto", "GRCh37") # the step-4 helper
body = { "task" => "review", "records" => eng["records"], "target" => "region",
"contig_style" => "ensembl", "source_basis" => "auto", "assembly" => "GRCh37",
"pipeline_hint" => "GATK -L targets.list", "facts" => eng["facts"] }
state = run_and_wait(body)
abort "review did not complete: #{state['status']} truncated=#{state['truncated']}" unless state["status"] == "succeeded" && !state["truncated"]
reply = parse_reply(state["output"]["output"])
problems = check_reply(reply, body)
abort "reply disagrees with the engine: #{problems.join('; ')}" unless problems.empty?
blockers = reply["issues"].select { |i| i["severity"] == "blocker" }
blockers.each { |i| warn " #{i['id']} lines #{i['lines']}: #{i['what']}\n fix: #{i['fix']}" }
puts "verdict: #{reply['verdict']} - #{reply['verdict_reason']}"
exit 2 if reply["verdict"] == "stop_and_check" || blockers.any?
<?php
$eng = engine(file_get_contents("targets.bed"), "region", "ensembl", "auto", "GRCh37"); // the step-4 helper
$body = ["task" => "review", "records" => $eng["records"], "target" => "region",
"contig_style" => "ensembl", "source_basis" => "auto", "assembly" => "GRCh37",
"pipeline_hint" => "GATK -L targets.list", "facts" => $eng["facts"]];
$state = runAndWait($body);
if ($state["status"] !== "succeeded" || !empty($state["truncated"])) { fwrite(STDERR, "review did not complete\n"); exit(1); }
$reply = parseReply($state["output"]["output"]);
$problems = checkReply($reply, $body);
if ($problems) { fwrite(STDERR, "reply disagrees with the engine: " . implode("; ", $problems) . "\n"); exit(1); }
$blockers = array_filter($reply["issues"], fn($i) => $i["severity"] === "blocker");
foreach ($blockers as $i) fwrite(STDERR, " {$i['id']} lines " . implode(",", $i["lines"]) . ": {$i['what']}\n fix: {$i['fix']}\n");
echo "verdict: {$reply['verdict']} - {$reply['verdict_reason']}\n";
if ($reply["verdict"] === "stop_and_check" || $blockers) exit(2);
var eng = Engine(File.ReadAllText("targets.bed"), "region", "ensembl", "auto", "GRCh37"); // the step-4 helper
var body = new Dictionary<string, string>
{
["task"] = "review", ["records"] = eng.GetProperty("records").GetString()!, ["target"] = "region",
["contig_style"] = "ensembl", ["source_basis"] = "auto", ["assembly"] = "GRCh37",
["pipeline_hint"] = "GATK -L targets.list", ["facts"] = eng.GetProperty("facts").GetString()!,
};
var state = await RunAndWait(body);
if (state.GetProperty("status").GetString() != "succeeded" || (state.TryGetProperty("truncated", out var tr) && tr.GetBoolean()))
{ Console.Error.WriteLine("review did not complete"); return 1; }
var reply = ParseReply(state.GetProperty("output").GetProperty("output").GetString()!);
var problems = CheckReply(reply, body);
if (problems.Count > 0) { Console.Error.WriteLine("reply disagrees with the engine: " + string.Join("; ", problems)); return 1; }
var blockers = reply.GetProperty("issues").EnumerateArray()
.Where(i => i.GetProperty("severity").GetString() == "blocker").ToList();
foreach (var i in blockers)
Console.Error.WriteLine($" {i.GetProperty("id")} lines {i.GetProperty("lines")}: {i.GetProperty("what")}\n fix: {i.GetProperty("fix")}");
Console.WriteLine($"verdict: {reply.GetProperty("verdict")} - {reply.GetProperty("verdict_reason")}");
return reply.GetProperty("verdict").GetString() == "stop_and_check" || blockers.Count > 0 ? 2 : 0;
Truncation and partial results
When the balance sits between min_credits and hold_credits, the run is not
refused: it executes with a reduced output cap and comes back with truncated: true on
the finished job and on the streaming done event. What you hold then is a prefix of the
reply. The keys come in contract order, so a truncated review may carry the title,
summary, verdict, source, assembly and
coverage while issues, steps and checks_after —
the part that tells you what to fix and how — are absent or cut mid-string.
Check the flag before you treat a reply as complete. A truncated review whose
verdict reads convert_with_fixes and whose issues list broke off after one
entry parses cleanly once repaired and looks like a short review; a gate reading it would pass a
file whose blocker was never written. The page's answer is to close the JSON structure that arrived,
render every section that parsed, and say the stream ended early above it; a script should refuse
the result or retry. The right retry is a top-up or a smaller paste with the attempt suffix on the
Idempotency-Key incremented — never a repair that appends brackets and calls the result
a review.
One more honest limit: the model sees the paste clipped from the middle at 24,000
characters, whole lines at a time, with a marker line where the cut was. The engine reads the whole
paste, so the facts — counts, flags, the assembly evidence — still describe every line; but the
model can only quote what survived, and it says so in notes_on_input. For a file much
larger than a paste, run the engine over all of it as a free gate and send the review a
representative slice.