Drive Slide Run Planner for PathML from your own code
Slide Run Planner for PathML checks a computational-pathology plan before any tiling. In the page, JavaScript ports of
three command-line tools bundled with the pathml agent skill run on your inputs:
slide_manifest.py validate (pseudonymous ids, unique slides and paths, no URLs, allowed
splits, no patient in two splits), plan_pipeline.py (PathML 3.0.5 tile counts and
uncompressed payload) and plan_inference.py (batch memory from numbers or a JSON model
card). The same tools run on your machine
(python scripts/plan_pipeline.py --width=98304 --height=77824 --tile-size=512 --stride=512),
and the page's "Copy commands" button gives you the exact lines for your inputs.
The metered lanes read only counts, tool reports and the page's flags: review judges what
blocks a bounded pilot and returns the pilot with PathML 3.0.5 code; methods drafts a
methods paragraph and a 16-item provenance record. Neither ever sees a slide, a file name, a slide id
or a patient id. Research use only: nothing here supports a diagnosis.
Two lanes: the task field
| task | send | you get back |
|---|---|---|
review | facts; context and question optional | status (blocked, fix_before_pilot, ready_for_pilot), a response to every flag, findings (ref, area, severity, evidence, action), a pilot_plan (scope, steps, checks), pilot_code, open_questions and assumptions. |
methods | facts; context, question and plan_review optional | status (draft_complete, draft_with_gaps, cannot_draft), flag responses, methods_paragraph, provenance (16 items: item, value, source = facts | context | to_fill), limitations, open_questions and assumptions. |
Worked example: review
The H&E example on the page: one patient has slides in train and test, one row has no split, one path is outside the project folder, and the model's input is 256 px while the tiles are 512 px.
{
"task": "review",
"facts": "{\"page\":\"pathml-desk\",\"skill\":\"@k-dense-ai/pathml (scripts run in the browser: slide_manifest.py validate, plan_pipeline.py, plan_inference.py)\",\"pathml_version_modelled\":\"3.0.5\",\"assumptions\":[\"The browser cannot open the slide files: every path was checked lexically only (URL, NUL byte, ~ expansion, normalisation, escape from --root, duplicates, suffix) and assumed to exist as a regular, non-symlink file within --max-slide-gib.\",\"Tile counts and payloads are the skill's dry-run arithmetic for PathML 3.0.5; they do not open a slide, measure compression or predict transform/model workspace memory.\",\"The inference plan reads numbers and a JSON model card only; no checkpoint was opened.\"],\"manifest\":{\"script\":\"slide_manifest.py\",\"exit\":2,\"error\":\"row 7: patient_id appears in multiple splits: 'train' and 'test'\",\"page_pass\":{\"rows\":12,\"columns\":[\"slide_id\",\"patient_id\",\"path\",\"split\",\"width\",\"height\"],\"has_split_column\":true,\"rows_without_split\":1,\"slides_by_split\":{\"train\":5,\"test\":4,\"validation\":2},\"patients_by_split_single\":{\"train\":3,\"validation\":2,\"test\":3},\"patients_in_multiple_splits\":1,\"problem_rows\":2,\"issues_by_kind\":{\"patient_multi_split\":1,\"path_escapes_root\":1},\"issues\":[{\"row\":7,\"kind\":\"patient_multi_split\"},{\"row\":13,\"kind\":\"path_escapes_root\"}],\"issues_omitted\":0,\"identifier_hints\":{\"mrn_like\":1},\"identifier_hint_rows\":[11],\"path_hints\":1,\"path_hint_rows\":[10]}},\"tiling\":{\"script\":\"plan_pipeline.py\",\"exit\":0,\"argv\":[\"--width=98304\",\"--height=77824\",\"--tile-size=512\",\"--stride=512\",\"--pipeline=BoxBlur, TissueDetectionHE, StainNormalizationHE\",\"--mpp-x=0.2527\",\"--mpp-y=0.2527\"],\"report\":{\"pathml_version_modelled\":\"3.0.5\",\"input_level0_shape_hw\":[77824,98304],\"level_downsample\":1,\"planned_level_shape_hw\":[77824,98304],\"tile_size_hw\":[512,512],\"stride_ij\":[512,512],\"pad\":false,\"tiles_ij\":[152,192],\"tile_count\":29184,\"overlap_pixels_per_axis\":0,\"gap_pixels_per_axis\":0,\"pipeline\":[{\"name\":\"BoxBlur\",\"kind\":\"image\"},{\"name\":\"TissueDetectionHE\",\"kind\":\"mask\"},{\"name\":\"StainNormalizationHE\",\"kind\":\"image\"}],\"expected_mask_outputs_per_tile\":1,\"expected_label_outputs_per_tile\":0,\"expected_count_outputs_per_tile\":0,\"input_image_bytes_per_tile\":786432,\"estimated_uncompressed_payload_gib\":28.5,\"physical_tile_size_um_yx\":[129.3824,129.3824],\"within_bounds\":true,\"warnings\":[],\"note\":\"This is a dry-run estimate. It does not open a slide, import PathML, measure compression, or predict transform/model workspace memory.\"}},\"per_slide\":{\"slides_with_size\":12,\"planned\":12,\"refused\":0,\"zero_tile_slides\":0,\"total_tiles\":350556,\"total_payload_gib\":342.339844,\"min_tiles\":23520,\"max_tiles\":36960,\"capped\":false},\"inference\":{\"script\":\"plan_inference.py\",\"exit\":0,\"argv\":[\"--tile-count=29184\",\"--batch-size=64\",\"--root=<project root>\",\"--model-card=model_card.json\",\"--max-memory-mib=8192\"],\"tile_count_from\":\"tiling\",\"report\":{\"model_card_used\":true,\"model_id\":\"hovernet-pannuke-fast\",\"artifact_sha256\":null,\"checkpoint_or_model_loaded\":false,\"input_shape_chw\":[3,256,256],\"dtype\":\"float32\",\"bytes_per_element\":4,\"tile_count\":29184,\"requested_batch_size\":64,\"number_of_batches\":456,\"final_batch_size\":64,\"input_bytes_per_tile\":786432,\"output_bytes_per_tile\":1310720,\"activation_bytes_per_tile_estimate\":18874368,\"persistent_bytes_estimate\":686870912,\"planned_peak_memory_mib\":1935.051147,\"memory_budget_mib\":8192,\"within_memory_budget\":true,\"recommended_max_batch_under_estimate\":376,\"estimated_total_output_gib\":35.625,\"warnings\":[\"Estimate excludes allocator fragmentation, framework caches, graph workspace, postprocessing, stitching, and concurrent workers.\"],\"note\":\"This planner reads numbers or strict JSON metadata only. It never opens model/checkpoint artifacts or imports PathML/Torch/ONNX.\"}},\"flags\":[{\"id\":\"F1\",\"kind\":\"manifest_refused\",\"severity\":\"high\",\"area\":\"manifest\",\"text\":\"slide_manifest.py refused the manifest: row 7: patient_id appears in multiple splits: 'train' and 'test'. The page's pass over every row found: 1 path outside --root.\"},{\"id\":\"F2\",\"kind\":\"leak_patient_split\",\"severity\":\"high\",\"area\":\"split\",\"text\":\"1 patient(s) have slides in more than one split. Tiles from one patient on both sides of the split make a held-out score optimistic.\"},{\"id\":\"F3\",\"kind\":\"rows_without_split\",\"severity\":\"medium\",\"area\":\"split\",\"text\":\"1 of 12 row(s) have no split value; the tool skips them in split_counts and in the isolation check.\"},{\"id\":\"F4\",\"kind\":\"identifier_hint\",\"severity\":\"medium\",\"area\":\"deidentification\",\"text\":\"Some slide/patient ids look like real identifiers (1 mrn-like). The tool checks syntax only; confirm they are pseudonyms.\"},{\"id\":\"F5\",\"kind\":\"path_hint\",\"severity\":\"medium\",\"area\":\"deidentification\",\"text\":\"1 path(s) contain a date-, record-number- or accession-like token. Keep direct identifiers out of file names.\"},{\"id\":\"F6\",\"kind\":\"no_checksum\",\"severity\":\"medium\",\"area\":\"provenance\",\"text\":\"The model card has no artifact_sha256; verify and record the checkpoint's checksum before loading it.\"},{\"id\":\"F7\",\"kind\":\"shape_mismatch\",\"severity\":\"medium\",\"area\":\"inference\",\"text\":\"The model expects 256 x 256 inputs but the tiles are 512 x 512: resize in a documented step or retile.\"}],\"browser_status\":\"blocked\"}",
"context": "Twelve H&E whole-slide images scanned at 40x on two scanners. We want to train a tumour-region classifier and report a held-out test score, then run nuclear segmentation with a HoVer-Net checkpoint on one 8 GB GPU.",
"question": "Is this plan safe to start a pilot on?"
}
The reply's status is blocked: a tool refusal and a patient-split leak can only be fixed, not argued away. Load the example on the page to see a saved reply in full, free.
Worked example: methods
The CODEX example: eight regions from seven donors, split by donor, tiled at 256 px with two collapsed channels, and a locally stored Mesmer ONNX export with a checksum in its model card.
{
"task": "methods",
"facts": "{\"page\":\"pathml-desk\",\"skill\":\"@k-dense-ai/pathml (scripts run in the browser: slide_manifest.py validate, plan_pipeline.py, plan_inference.py)\",\"pathml_version_modelled\":\"3.0.5\",\"assumptions\":[\"The browser cannot open the slide files: every path was checked lexically only (URL, NUL byte, ~ expansion, normalisation, escape from --root, duplicates, suffix) and assumed to exist as a regular, non-symlink file within --max-slide-gib.\",\"Tile counts and payloads are the skill's dry-run arithmetic for PathML 3.0.5; they do not open a slide, measure compression or predict transform/model workspace memory.\",\"The inference plan reads numbers and a JSON model card only; no checkpoint was opened.\"],\"manifest\":{\"script\":\"slide_manifest.py\",\"exit\":0,\"report\":{\"row_count\":8,\"patient_count\":7,\"split_counts\":{\"test\":2,\"train\":4,\"validation\":2},\"suffix_counts\":{\".ome.tiff\":2,\".qptiff\":6},\"checks\":{\"local_regular_files\":true,\"no_urls\":true,\"no_symlinks\":true,\"unique_slide_ids\":true,\"unique_slide_paths\":true,\"patient_split_isolation\":true}},\"page_pass\":{\"rows\":8,\"columns\":[\"slide_id\",\"patient_id\",\"path\",\"split\"],\"has_split_column\":true,\"rows_without_split\":0,\"slides_by_split\":{\"train\":4,\"validation\":2,\"test\":2},\"patients_by_split_single\":{\"train\":3,\"validation\":2,\"test\":2},\"patients_in_multiple_splits\":0,\"problem_rows\":0,\"issues_by_kind\":{},\"issues\":[],\"issues_omitted\":0,\"identifier_hints\":{},\"identifier_hint_rows\":[],\"path_hints\":0,\"path_hint_rows\":[]}},\"tiling\":{\"script\":\"plan_pipeline.py\",\"exit\":0,\"argv\":[\"--width=24576\",\"--height=20480\",\"--tile-size=256\",\"--stride=256\",\"--channels=2\",\"--bytes-per-channel=2\",\"--pipeline=CollapseRunsCODEX, RescaleIntensity\",\"--mpp-x=0.377\",\"--mpp-y=0.377\"],\"report\":{\"pathml_version_modelled\":\"3.0.5\",\"input_level0_shape_hw\":[20480,24576],\"level_downsample\":1,\"planned_level_shape_hw\":[20480,24576],\"tile_size_hw\":[256,256],\"stride_ij\":[256,256],\"pad\":false,\"tiles_ij\":[80,96],\"tile_count\":7680,\"overlap_pixels_per_axis\":0,\"gap_pixels_per_axis\":0,\"pipeline\":[{\"name\":\"CollapseRunsCODEX\",\"kind\":\"image\"},{\"name\":\"RescaleIntensity\",\"kind\":\"image\"}],\"expected_mask_outputs_per_tile\":0,\"expected_label_outputs_per_tile\":0,\"expected_count_outputs_per_tile\":0,\"input_image_bytes_per_tile\":262144,\"estimated_uncompressed_payload_gib\":1.875,\"physical_tile_size_um_yx\":[96.512,96.512],\"within_bounds\":true,\"warnings\":[],\"note\":\"This is a dry-run estimate. It does not open a slide, import PathML, measure compression, or predict transform/model workspace memory.\"}},\"inference\":{\"script\":\"plan_inference.py\",\"exit\":0,\"argv\":[\"--tile-count=7680\",\"--batch-size=16\",\"--root=<project root>\",\"--model-card=model_card.json\",\"--max-memory-mib=6144\"],\"tile_count_from\":\"tiling\",\"report\":{\"model_card_used\":true,\"model_id\":\"mesmer-local-onnx\",\"artifact_sha256\":\"3f1c9a7be2d04c5e8a61b0f7d9e2c4a1b5f6e7d8c9a0b1c2d3e4f5a6b7c8d9e0\",\"checkpoint_or_model_loaded\":false,\"input_shape_chw\":[2,256,256],\"dtype\":\"float32\",\"bytes_per_element\":4,\"tile_count\":7680,\"requested_batch_size\":16,\"number_of_batches\":480,\"final_batch_size\":16,\"input_bytes_per_tile\":524288,\"output_bytes_per_tile\":524288,\"activation_bytes_per_tile_estimate\":8388608,\"persistent_bytes_estimate\":831306368,\"planned_peak_memory_mib\":936.795532,\"memory_budget_mib\":6144,\"within_memory_budget\":true,\"recommended_max_batch_under_estimate\":594,\"estimated_total_output_gib\":3.75,\"warnings\":[\"Estimate excludes allocator fragmentation, framework caches, graph workspace, postprocessing, stitching, and concurrent workers.\"],\"note\":\"This planner reads numbers or strict JSON metadata only. It never opens model/checkpoint artifacts or imports PathML/Torch/ONNX.\"}},\"flags\":[{\"id\":\"F1\",\"kind\":\"bioformats_backend\",\"severity\":\"low\",\"area\":\"tiling\",\"text\":\"8 slide(s) need the Bio-Formats backend (Java) rather than OpenSlide.\"},{\"id\":\"F2\",\"kind\":\"no_mask_stage\",\"severity\":\"low\",\"area\":\"tiling\",\"text\":\"No mask stage (such as TissueDetectionHE or BinaryThreshold): background tiles are processed like tissue, and no QC mask is recorded.\"}],\"browser_status\":\"pilot_ready\"}",
"context": "Eight CODEX regions from seven donors, nuclear and membrane channels collapsed to two. Whole-cell segmentation with a locally stored Mesmer ONNX export, then per-cell quantification.",
"question": "Keep it to one paragraph a reviewer can check."
}
Input fields
Every field is a string; facts is JSON text.
| field | type | required | meaning |
|---|---|---|---|
task | string | yes | "review" or "methods". |
facts | string | yes | The tool reports, counts and flags, as JSON text (below). |
context | string | no | The cohort, the goal and the hardware, without patient details. Sent as written, up to about 4,000 characters. |
question | string | no | A question to answer in the reply. |
plan_review | string | no | Methods lane: an earlier plan review as text (the page fills it from the review's status, findings and pilot). |
retry_note | string | no | Only on a reformat retry. |
The facts string
assumptions (what the browser could not check); manifest (script, exit,
error with file names replaced by '<name>', report when exit is 0 with row_count,
patient_count, split_counts, suffix_counts and checks, and page_pass: rows, columns, has_split_column,
rows_without_split, slides_by_split, patients_by_split_single, patients_in_multiple_splits, problem_rows,
issues_by_kind, issues as row numbers and kinds, identifier_hints and identifier_hint_rows, path_hints and
path_hint_rows); tiling (argv, exit, error or the full
plan_pipeline.py report); per_slide when the manifest has width and height (slides_with_size,
planned, refused, zero_tile_slides, total_tiles, total_payload_gib, min_tiles, max_tiles, capped);
inference (argv with the root replaced, tile_count_from, exit,
error or the full plan_inference.py report); flags (id,
kind, severity, area, text); and browser_status
(blocked, fix_first, pilot_ready). Build it with the skill's own scripts, or copy
"Download .json" from a page run (it includes facts_sent).
The output
One JSON object, serialised as a string at data.output.output. Keys in both lanes: task, status, headline, flag_responses (ref, stance = confirmed | explained | dismissed | needs_owner, note), open_questions, assumptions. Lane keys are listed in the table above.
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": {"job_id": "job_...", "status": "queued"}}
{"ok": false, "error": {"code": "payment_required", "message": "..."}}
The token is minted for this app (the guest endpoint takes {"slug":"pathml-desk"} in
its body), so no slug header is needed afterwards. Send it as Authorization: Bearer ....
The input object IS the request body. There is no {"input": ...}
wrapper. A wrapped body is answered with an unknown field 'input' warning, and the
model never sees your text.
Error codes
| status | code | what to do |
|---|---|---|
| 400 | validation_error | A field is missing or the wrong type. Every field is a string: facts must be a JSON-encoded string, not an object. |
| 401 | unauthorized | The token is missing, malformed or expired. Get a new one from the token page. |
| 402 | payment_required | The balance is below min_credits. Call /estimate first and top up. |
| 403 | forbidden | The token is valid but not for this app, or a guest token tried a metered run. A guest cannot run; sign in for a personal token. |
| 404 | not_found | Unknown job id, or the app slug does not exist. |
| 409 | conflict | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
| 429 | rate_limited | Too many requests. Back off and retry; do not tight-loop. |
| 5xx | internal | A server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice. |
1. A tiny client
One helper that sends the token, unwraps data and raises on ok: false.
The token comes from the token page (Copy token or
Copy shell export); step 2 covers the kinds of token and minting one from code.
# 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="pathml-desk"
TOKEN="$SKILLSAFE_TOKEN" # from https://pathml-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 json, os, urllib.error, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "pathml-desk"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://pathml-desk.skillsafe.ai/tokens.html
def call(path, body=None):
"""Returns the unwrapped `data`, or raises with the API error code."""
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")
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"]
import { readFileSync } from "node:fs";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "pathml-desk";
// Paste the token from https://pathml-desk.skillsafe.ai/tokens.html into a file named "token",
// or replace the fallback with it.
let TOKEN = "YOUR_TOKEN";
try { TOKEN = readFileSync("token", "utf8").trim(); } catch {}
async function call(path, body) {
const res = await fetch(`${BASE}/${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
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"
"net/http"
"os"
"strings"
"time"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "pathml-desk"
)
var token = os.Getenv("SKILLSAFE_TOKEN") // from https://pathml-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"`
}
func call(path string, body any) (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")
}
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
}
import java.net.URI;
import java.net.http.*;
public class EdaDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "pathml-desk";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN);
if (jsonBody != null) {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
b.GET();
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is always {"ok":true,"data":...} or {"ok":false,"error":...}.
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "pathml-desk"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://pathml-desk.skillsafe.ai/tokens.html
def call(path, body = nil)
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 = body.to_json
end
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 = "pathml-desk";
define("TOKEN", getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"); // from /tokens.html
function call(string $path, ?array $body = null) {
$ch = curl_init(BASE . "/" . $path);
$headers = ["Authorization: Bearer " . TOKEN];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
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.Text.Json;
static class EdaDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "pathml-desk";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(string path, object? body = null)
{
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, $"{Base}/{path}");
req.Headers.Add("Authorization", $"Bearer {Token}");
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");
}
}
2. 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. A guest token, minted with
POST /guest and {"slug":"pathml-desk"}, can call /me and
/estimate; the run is metered, so /run and /run-stream need
a personal token.
# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
# https://pathml-desk.skillsafe.ai/tokens.html
# export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead. A guest token is enough
# for /me and /estimate; a run needs a personal token from signing in.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" -d '{"slug":"pathml-desk"}'
# {"ok":true,"data":{"token":"...","subject_type":"guest"}}
# Open https://pathml-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 start a metered run.
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest", data=b'{"slug": "pathml-desk"}', method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
// Open https://pathml-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 start a metered run.
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "pathml-desk" }),
});
const TOKEN = (await res.json()).data.token;
// Open https://pathml-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 start a metered run.
guestReq, _ := http.NewRequest(http.MethodPost,
"https://api.skillsafe.ai/v1/app-api/guest", bytes.NewReader([]byte(`{"slug":"pathml-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://pathml-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 start a metered run.
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\":\"pathml-desk\"}"))
.build();
HttpResponse<String> guest = http.send(guestReq, HttpResponse.BodyHandlers.ofString());
System.out.println(guest.body()); // {"ok":true,"data":{"token":"...","subject_type":"guest"}}
# Open https://pathml-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 start a metered run.
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 = { slug: "pathml-desk" }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
TOKEN = JSON.parse(res.body)["data"]["token"]
<?php
// Open https://pathml-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 start a metered run.
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["slug" => "pathml-desk"]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$guest = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $guest["data"]["token"];
// Open https://pathml-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 start a metered run.
using var http = new HttpClient();
var guestReq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/guest");
guestReq.Content = new StringContent("{\"slug\":\"pathml-desk\"}", Encoding.UTF8, "application/json");
var guestRes = await http.SendAsync(guestReq);
var guest = await guestRes.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(guest.GetProperty("data").GetProperty("token").GetString());
3. Check the session and the balance
call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
print(me["subject_type"], me.get("credits"))
const me = await call("me");
console.log(me.subject_type, me.credits);
raw, err := call("me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("me", null));
// {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var me = await EdaDesk.Call("me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
4. Price the run (free)
/estimate returns the model binding and the credits a run would reserve. It creates no
job and charges nothing. Expect model_alias gpt-terra.
hold_credits is a reservation, not the price. min_credits is
the least balance that can start a run. What you pay is charged_credits, reported on the
finished job, usually far lower. The body is the input object itself, with no
{"input": ...} wrapper. /estimate does no input validation,
so check the shape yourself: an object whose every value is a string, task equal to
review or methods, and a facts that is a JSON string parsing to an object.
# body.json is the input object itself - no {"input": ...} wrapper. estimate does
# not validate it, so check the shape first:
python3 -c '
import json
b = json.load(open("body.json"))
assert isinstance(b, dict) and b.get("task") in ("review", "methods")
assert all(isinstance(v, str) for v in b.values())
assert isinstance(json.loads(b["facts"]), dict)
'
INPUT=$(cat body.json)
call estimate "$INPUT"
# {"ok":true,"data":{"model":"...","model_alias":"gpt-terra",
# "markup_bps":...,"hold_credits":...,"min_credits":...,"sponsor_enabled":false}}
# hold_credits is RESERVED, not the price; charged_credits after the run is the cost.
INPUT = json.load(open("body.json")) # the input object itself, no wrapper
assert isinstance(INPUT, dict) and INPUT.get("task") in ("review", "methods")
assert all(isinstance(v, str) for v in INPUT.values()), "every field is a string"
assert isinstance(json.loads(INPUT["facts"]), dict), "facts is a JSON string of an object"
est = call("estimate", INPUT)
print(est["model_alias"], "reserves", est["hold_credits"], "credits (not the price)")
const INPUT = JSON.parse(readFileSync("body.json", "utf8")); // no {input: ...} wrapper
if (!INPUT || typeof INPUT !== "object" || !["review", "methods"].includes(INPUT.task)) throw new Error("task must be review or methods");
if (!Object.values(INPUT).every((v) => typeof v === "string")) throw new Error("every field is a string");
if (typeof JSON.parse(INPUT.facts) !== "object") throw new Error("facts is a JSON string of an object");
const est = await call("estimate", INPUT);
console.log(est.model_alias, "reserves", est.hold_credits, "credits (not the price)");
raw, _ := os.ReadFile("body.json")
var input map[string]string // every field is a string; Unmarshal fails otherwise
if err := json.Unmarshal(raw, &input); err != nil {
panic("body.json must be an object of strings: " + err.Error())
}
if input["task"] != "review" && input["task"] != "methods" {
panic("task must be review or methods")
}
var facts map[string]any
if err := json.Unmarshal([]byte(input["facts"]), &facts); err != nil {
panic("facts must be a JSON string of an object")
}
est := call("estimate", input)
fmt.Println(est["model_alias"], "reserves", est["hold_credits"], "credits (not the price)")
String input = Files.readString(Path.of("body.json")); // the input object itself
if (!input.matches("(?s)\\s*\\{.*\"task\"\\s*:\\s*\"(review|methods)\".*\\}\\s*"))
throw new IllegalStateException("body.json must be an object with task review or methods");
if (!input.contains("\"facts\""))
throw new IllegalStateException("both lanes need facts");
System.out.println(call("estimate", input)); // hold_credits is a reservation, not the price
INPUT = JSON.parse(File.read("body.json")) # no {"input": ...} wrapper
raise "task must be review or methods" unless %w[review methods].include?(INPUT["task"])
raise "every field is a string" unless INPUT.values.all? { |v| v.is_a?(String) }
raise "facts is a JSON string of an object" unless JSON.parse(INPUT["facts"]).is_a?(Hash)
est = call("estimate", INPUT)
puts "#{est['model_alias']} reserves #{est['hold_credits']} credits (not the price)"
$input = json_decode(file_get_contents("body.json"), true); // no {"input": ...} wrapper
if (!is_array($input) || !in_array($input["task"] ?? "", ["review", "methods"], true)) { throw new Exception("task must be review or methods"); }
foreach ($input as $v) { if (!is_string($v)) { throw new Exception("every field is a string"); } }
if (!is_array(json_decode($input["facts"] ?? "", true))) { throw new Exception("facts is a JSON string of an object"); }
$est = call("estimate", $input);
echo $est["model_alias"], " reserves ", $est["hold_credits"], " credits (not the price)\n";
var input = File.ReadAllText("body.json"); // the input object itself
using var doc = JsonDocument.Parse(input);
var root = doc.RootElement;
var lane = root.GetProperty("task").GetString();
if (lane != "review" && lane != "methods") throw new Exception("task must be review or methods");
foreach (var p in root.EnumerateObject())
if (p.Value.ValueKind != JsonValueKind.String) throw new Exception("every field is a string");
JsonDocument.Parse(root.GetProperty("facts").GetString()!); // throws unless facts is JSON
Console.WriteLine(await Call("estimate", input)); // hold_credits is a reservation, not the price
5. Run it, then poll
POST /run returns a job_id; poll GET /jobs/{id} until it is
terminal. The reply is a string at data.output.output: JSON.parse
it (step 7). Send an Idempotency-Key built from the lane, a hash of the input and the
attempt number, pathml-desk:<lane>:<hash>:a<attempt>, so a retried
request returns the same job instead of billing a second run. Use one key per distinct input: edited
facts, context, plan_review or question are a new hash, and replaying an old key with a different
body is a 409. Any stable digest of the body works. Leave retry_note out of
the hash and bump the attempt instead.
# Always send an Idempotency-Key derived from the input. A retried request with
# the same key returns the SAME job instead of billing a second run.
LANE=$(printf '%s' "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["task"])') # review or methods
KEY="pathml-desk:$LANE:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
while :; do
OUT=$(call "jobs/$JOB")
STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
sleep 2
done
# {"ok":true,"data":{"job_id":"job_...","status":"succeeded",
# "output":{"output":"{\"task\":\"review\",\"status\":\"blocked\",\"headline\":\"...\", ...}"},
# "charged_credits":...,"truncated":false}}
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])' > reply.json
import hashlib, time
digest = hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:16]
key = f"pathml-desk:{INPUT['task']}:{digest}:a1"
req = urllib.request.Request(f"{BASE}/run", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call(f"jobs/{job_id}")
if job["status"] in ("succeeded", "failed"):
break
time.sleep(2)
if job["status"] == "failed":
raise RuntimeError(job.get("error"))
text = job["output"]["output"] # the reply, as a string
print("charged", job.get("charged_credits"), "truncated", job.get("truncated"))
import { createHash } from "node:crypto";
const digest = createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16);
const key = `pathml-desk:${INPUT.task}:${digest}:a1`;
const started = await fetch(`${BASE}/run`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify(INPUT),
}).then((r) => r.json());
if (!started.ok) throw new Error(`${started.error.code}: ${started.error.message}`);
let job = started.data;
while (job.status !== "succeeded" && job.status !== "failed") {
await new Promise((r) => setTimeout(r, 2000));
job = await call(`jobs/${job.job_id}`);
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const text = job.output.output; // the reply, as a string
console.log(job.charged_credits, job.truncated);
body, _ := json.Marshal(input)
sum := sha256.Sum256(body)
key := fmt.Sprintf("pathml-desk:%s:%x:a1", input["task"], sum[:8])
req, _ := http.NewRequest(http.MethodPost, base+"/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
var started struct {
Data struct {
JobID string `json:"job_id"`
} `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&started)
res.Body.Close()
var jobOutput string
for {
raw, err := call("jobs/"+started.Data.JobID, nil)
if err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
Charged int `json:"charged_credits"`
Truncated bool `json:"truncated"`
}
_ = json.Unmarshal(raw, &job)
if job.Status == "succeeded" {
jobOutput = job.Output.Output
fmt.Println(job.Charged, job.Truncated)
break
}
if job.Status == "failed" {
panic(string(raw))
}
time.Sleep(2 * time.Second)
}
String key = "pathml-desk:" + lane + ":" + sha256Hex(input).substring(0, 16) + ":a1";
HttpRequest run = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(input)).build();
String started = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
String jobId = started.replaceAll(".*\"job_id\":\"([^\"]+)\".*", "$1");
while (true) {
String job = call("jobs/" + jobId, null);
if (job.contains("\"status\":\"succeeded\"")) { System.out.println(job); break; }
if (job.contains("\"status\":\"failed\"")) throw new RuntimeException(job);
Thread.sleep(2000);
}
// Parse data.output.output (a string holding the reply JSON) with your JSON library.
// sha256Hex: HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(input.getBytes(UTF_8)))
require "digest"
key = "pathml-desk:#{INPUT['task']}:#{Digest::SHA256.hexdigest(INPUT.to_json)[0, 16]}:a1"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = INPUT.to_json
job = JSON.parse(Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }.body)["data"]
until %w[succeeded failed].include?(job["status"])
sleep 2
job = call("jobs/#{job['job_id']}")
end
raise job.inspect if job["status"] == "failed"
text = job["output"]["output"] # the reply, as a string
puts job["charged_credits"], job["truncated"]
<?php
$key = "pathml-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json", "Idempotency-Key: " . $key],
CURLOPT_RETURNTRANSFER => true,
]);
$job = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
while (!in_array($job["status"], ["succeeded", "failed"], true)) {
sleep(2);
$job = call("jobs/" . $job["job_id"]);
}
if ($job["status"] === "failed") { throw new RuntimeException(json_encode($job)); }
$text = $job["output"]["output"]; // the reply, as a string
echo $job["charged_credits"], PHP_EOL;
using System.Security.Cryptography;
var json = input; // the body.json text from step 4
var key = $"pathml-desk:{lane}:" + Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(json)))[..16].ToLower() + ":a1";
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run");
req.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN"}");
req.Headers.Add("Idempotency-Key", key);
req.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var started = await (await new HttpClient().SendAsync(req)).Content.ReadFromJsonAsync<JsonElement>();
var jobId = started.GetProperty("data").GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await EdaDesk.Call($"jobs/{jobId}");
var status = job.GetProperty("status").GetString();
if (status == "succeeded") break;
if (status == "failed") throw new Exception(job.ToString());
await Task.Delay(2000);
}
var output = job.GetProperty("output").GetProperty("output").GetString()!; // the reply, as a string
6. Or stream it
POST /run-stream takes the same body and headers and answers with server-sent events:
job (the job id), delta (chunks of the reply) and done (the
status, charged_credits, truncated and, when present, the full
output). A browser page may receive only tick heartbeats and then
done, never a delta, so take the reply from done.output.output
when it is there, fall back to the concatenated deltas, and fall back again to
GET /jobs/{id}.
# Server-sent events. `delta` events carry chunks of the reply; `done` carries the
# status, charged_credits and the truncated flag. Ignore `tick` heartbeats.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d "$INPUT"
# event: job {"job_id":"job_..."}
# event: delta {"text":"{\"task\":\"review\",\"status\":\"blocked\",\"headline\":\"The"}
# event: done {"status":"succeeded","charged_credits":...,"truncated":false}
req = urllib.request.Request(f"{BASE}/run-stream", data=json.dumps(INPUT).encode(), method="POST")
for h, v in (("Authorization", f"Bearer {TOKEN}"), ("Content-Type", "application/json"),
("Idempotency-Key", key), ("Accept", "text/event-stream")):
req.add_header(h, v)
raw, done, event = "", {}, None
with urllib.request.urlopen(req) as stream:
for line in stream:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
raw += json.loads(line[6:]).get("text", "")
elif line.startswith("data: ") and event == "done":
done = json.loads(line[6:])
text = (done.get("output") or {}).get("output") or raw
print(done.get("status"), done.get("charged_credits"), done.get("truncated"))
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key, Accept: "text/event-stream" },
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null, done = null;
for (;;) {
const { value, done: end } = await reader.read();
if (end) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i); buf = buf.slice(i + 1);
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ") && event === "delta") raw += JSON.parse(line.slice(6)).text || "";
else if (line.startsWith("data: ") && event === "done") done = JSON.parse(line.slice(6));
}
}
const streamed = done?.output?.output || raw; // browsers may get only ticks + done
console.log(done, streamed.length);
req, _ = http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, err = http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var raw strings.Builder
event := ""
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = line[7:]
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct{ Text string `json:"text"` }
_ = json.Unmarshal([]byte(line[6:]), &d)
raw.WriteString(d.Text)
case strings.HasPrefix(line, "data: ") && event == "done":
fmt.Println("done:", line[6:])
}
}
HttpRequest stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(input)).build();
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
// "event: delta" lines are followed by "data: {\"text\":...}"; "event: done" by the status.
if (line.startsWith("data: ")) System.out.println(line.substring(6));
});
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
{ "Authorization" => "Bearer #{TOKEN}", "Content-Type" => "application/json",
"Idempotency-Key" => key, "Accept" => "text/event-stream" }.each { |k, v| req[k] = v }
req.body = INPUT.to_json
raw, event = +"", nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |h|
h.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event: ") then event = line[7..]
elsif line.start_with?("data: ") && event == "delta" then raw << JSON.parse(line[6..])["text"].to_s
elsif line.start_with?("data: ") && event == "done" then puts line[6..]
end
end
end
end
end
<?php
$raw = ""; $event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json", "Idempotency-Key: " . $key, "Accept: text/event-stream"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) $event = substr($line, 7);
elseif (str_starts_with($line, "data: ") && $event === "delta") $raw .= json_decode(substr($line, 6), true)["text"] ?? "";
elseif (str_starts_with($line, "data: ") && $event === "done") echo substr($line, 6), PHP_EOL;
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
var sreq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream");
sreq.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN"}");
sreq.Headers.Add("Idempotency-Key", key);
sreq.Headers.Add("Accept", "text/event-stream");
sreq.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
using var sres = await new HttpClient().SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var sr = new StreamReader(await sres.Content.ReadAsStreamAsync());
var raw = new System.Text.StringBuilder(); string? ev = null, line;
while ((line = await sr.ReadLineAsync()) != null)
{
if (line.StartsWith("event: ")) ev = line[7..];
else if (line.StartsWith("data: ") && ev == "delta") raw.Append(JsonSerializer.Deserialize<JsonElement>(line[6..]).GetProperty("text").GetString());
else if (line.StartsWith("data: ") && ev == "done") Console.WriteLine(line[6..]);
}
7. Parse the reply
The reply is one JSON object serialised as a string. Parse it and check that task is the
lane you asked for. Read pilot_code before running it, on de-identified local copies only.
# The reply is a JSON string inside data.output.output (saved as reply.json in step 5):
python3 -c 'import json;r=json.load(open("reply.json"));print(r["task"],r["status"],r["headline"])'
python3 -c '
import json
r = json.load(open("reply.json"))
if r["task"] == "review":
for f in r["findings"]: print("-", f["severity"], f["area"], f["action"])
open("pilot.py", "w").write(r["pilot_code"]) # read it before running it
else:
for p in r["provenance"]: print(p["item"], "|", p["value"], "|", p["source"])
'
reply = json.loads(job["output"]["output"])
assert reply["task"] == INPUT["task"], "the model answered as another lane"
print(reply["status"], reply["headline"])
if reply["task"] == "review":
for f in reply["findings"]:
print("-", f["severity"], f["area"], f["evidence"], "->", f["action"])
open("pilot.py", "w").write(reply["pilot_code"]) # read it before running it
else:
print(reply["methods_paragraph"])
todo = [p["item"] for p in reply["provenance"] if p["source"] == "to_fill"]
print("to fill:", todo)
const reply = JSON.parse(job.output.output);
if (reply.task !== INPUT.task) throw new Error("the model answered as another lane");
console.log(reply.status, reply.headline);
if (reply.task === "review") {
for (const f of reply.findings) console.log("-", f.severity, f.area, f.action);
writeFileSync("pilot.py", reply.pilot_code); // read it before running it
} else {
console.log(reply.methods_paragraph);
console.log("to fill:", reply.provenance.filter(p => p.source === "to_fill").map(p => p.item));
}
var reply map[string]any
json.Unmarshal([]byte(job["output"].(map[string]any)["output"].(string)), &reply)
if reply["task"] != input["task"] {
panic("the model answered as another lane")
}
fmt.Println(reply["status"], reply["headline"])
if reply["task"] == "review" {
for _, f := range reply["findings"].([]any) {
fmt.Println("-", f.(map[string]any)["severity"], f.(map[string]any)["action"])
}
os.WriteFile("pilot.py", []byte(reply["pilot_code"].(string)), 0o644) // read it before running it
} else {
fmt.Println(reply["methods_paragraph"])
}
// With any JSON library, parse data.output.output (a string) into an object, then:
// reply.task must equal the task you sent;
// review -> reply.status, reply.findings[].action, reply.pilot_plan.steps[], reply.pilot_code
// methods -> reply.status, reply.methods_paragraph, reply.provenance[] (item, value, source)
String out = job.replaceAll("(?s).*\"output\"\\s*:\\s*\\{\\s*\"output\"\\s*:\\s*(\".*?(?<!\\\\)\").*", "$1");
System.out.println(out.substring(0, Math.min(200, out.length())));
reply = JSON.parse(job["output"]["output"])
raise "the model answered as another lane" unless reply["task"] == INPUT["task"]
puts reply["status"], reply["headline"]
if reply["task"] == "review"
reply["findings"].each { |f| puts "- #{f['severity']} #{f['area']}: #{f['action']}" }
File.write("pilot.py", reply["pilot_code"]) # read it before running it
else
puts reply["methods_paragraph"]
reply["provenance"].each { |p| puts "#{p['item']} | #{p['value']}" }
end
$reply = json_decode($job["output"]["output"], true);
if ($reply["task"] !== $input["task"]) { throw new Exception("the model answered as another lane"); }
echo $reply["status"], " ", $reply["headline"], "\n";
if ($reply["task"] === "review") {
foreach ($reply["findings"] as $f) { echo "- ", $f["severity"], " ", $f["action"], "\n"; }
file_put_contents("pilot.py", $reply["pilot_code"]); // read it before running it
} else {
echo $reply["methods_paragraph"], "\n";
}
using var rdoc = JsonDocument.Parse(outputString); // data.output.output
var reply = rdoc.RootElement;
if (reply.GetProperty("task").GetString() != lane) throw new Exception("the model answered as another lane");
Console.WriteLine($"{reply.GetProperty("status")} {reply.GetProperty("headline")}");
if (lane == "review")
{
foreach (var f in reply.GetProperty("findings").EnumerateArray())
Console.WriteLine("- " + f.GetProperty("action").GetString());
File.WriteAllText("pilot.py", reply.GetProperty("pilot_code").GetString()); // read it before running it
}
else
{
Console.WriteLine(reply.GetProperty("methods_paragraph").GetString());
}
Costs
- The three skill tools are free and run in the page; nothing is metered until you start a lane.
/estimateis free. It creates no job and returnshold_credits: a reservation held against your balance while the run executes, not the price.- A run is billed only for what it uses:
charged_creditson the finished job and in thedoneevent, usually far below the hold. - Sponsorship is off: every run is paid from the caller's own balance.
- Runs need a signed-in user token. A guest token can call
/meand/estimateonly; sign in for a personal token on the token page. - A reformat retry (with
retry_note) is a new attempt with its own key and its own charge.
Invariants worth asserting
- The reply is one JSON object whose
taskequals thetaskyou sent, with every key of that lane's contract present. - Every flag id in
facts.flagsis answered once inflag_responses, and no other id is; a flag whose kind ends in_refused, orleak_patient_split, is never dismissed or explained. - Review: the status is no looser than the stances imply (an open high flag means
blocked, an open medium flag means at mostfix_before_pilot); every confirmed or open flag has a finding;pilot_codeis empty when the tiling plan was refused, and otherwise uses the plan's tile size, stride and stages in order, bounded withislice. - Methods: never
draft_completewhenfacts.browser_statusisblocked;provenanceholds the 16 items in order; everyto_fillvalue starts with[to fill:; the limitations say the slide files were not opened and the tile counts are estimates. - Every number in the evidence appears in
facts,contextorquestion.