Drive Set Diff Desk from your own code
The page does its free work in the browser: it reads the output of terraform show -json, matches the
elements of every AzureRM Set-type attribute by its key, sets aside the elements that only moved (order-only entries,
N01..) and numbers every real change (findings, F01..): replacements and the attributes that
force them, creates, deletes, Set elements added, removed or modified, other attributes, and suspected reorders in lists
the skill's table does not know. Its paid review lane is available over HTTP. You send one JSON object with
task set to "review", and the model replies with one JSON object. The easiest way to build the
facts string is the page itself: SetKit.buildInput(SetKit.analyze(planText, {ignoreCase}), {environment, intent})
in setkit.js, which also runs under Node. It never puts the plan itself, its prior state,
variables or configuration into the input, and every value Terraform marked sensitive becomes (sensitive).
Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends
Content-Type: application/json and, once you have a token,
Authorization: Bearer <token>. The review runs on gpt-terra with a 10%
publisher markup. A run needs a signed-in (user) token and is metered: the platform holds
the estimate's hold_credits while the run is in flight and then charges only
what it used (charged_credits). /estimate is free. A plain-text summary is at
/llms.txt.
The response envelope
Every response is {"data": ..., "error": null} on success or {"data": null, "error": {"code", "message"}} on failure.
Error codes
| HTTP | code | meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is not a JSON object, or a field is the wrong type. Every field of the run input is a string. |
| 401 | UNAUTHORIZED | No token, or an expired one: get a new one (step 2). |
| 402 | INSUFFICIENT_CREDITS | The balance is below the run's minimum. Top up at skillsafe.ai/account/billing/. |
| 403 | FORBIDDEN | A guest token tried a metered run: sign in for a personal token. |
| 404 | NOT_FOUND | Unknown job id. |
| 429 | RATE_LIMITED | Back off and retry with the same Idempotency-Key. |
The task field
One lane: "review". The model answers every finding (confirmed or disputed, with a risk of high, medium,
low or none, what it does and why), confirms or disputes the order-only entries, writes one to six checks before or after
apply, proposes Set-table additions for the suspected reorders it confirms, compares the changes with your intent,
reports text in the plan addressed to reviewers, and writes a pull-request comment. Its verdict follows from the risks:
any high gives hold, any medium or low apply with checks, otherwise apply.
Input fields (all strings)
| field | when | content |
|---|---|---|
task | required | review |
environment | required | production, staging, development or unspecified. In production a replace or delete is always high. |
intent | optional | One line: what the change is meant to do (up to 400 characters). |
facts | required | A JSON string: plan{terraform_version, format_version, resources_total, changed, azurerm_changed, by_action}, counts, exit_code (the skill's: 2 a replacement, 1 a Set change or create/delete, 0 none), ignore_case, findings[{id, kind, severity, address, type, path, text, detail?, forced_by?}] (up to 80, every high one kept), noise[{id, address, path, count}] (up to 120), unknown[], injection[{address, path, text}], warnings[], table_types[], notes[]. |
retry_note | optional | Only on a reformat retry: what was wrong with the previous reply. |
The reply contract
task, task_note, verdict (apply | apply with checks | hold), headline,
intent{status: matches|partly|does not match|no intent given, note},
findings[{id, response: confirmed|disputed, risk: high|medium|low|none, what, why}] (one per finding, in order),
noise{status: confirmed|disputed, disputed_ids[], note},
checks[{check, when: before apply|after apply, ids[]}] (1-6),
table_additions[{id, resource_type, attribute, key, note}], injection_note,
pr_comment[] (3-12 lines, the first Verdict: and the verdict), questions[], summary.
Worked examples
These are the page's own bundled examples (invented companies, addresses and documentation-range IPs), with the input as app.js builds it and the saved model reply the page replays for free - one per verdict: apply with checks, apply, and hold.
Apply with checks: an edge gateway gains an API listener
Input (facts shortened here):
{
"task": "review",
"environment": "production",
"facts": "(a JSON string; decoded: 7 findings - F01 set-change/medium, F02 set-change/medium, F03 set-change/medium, F04 set-change/medium, F05 set-change/medium, F06 set-change/medium, F07 create/medium; 2 order-only entries; exit code 1)",
"intent": "Expose the new API backend on api.contoso.example through the edge gateway, port 8443 to the backends"
}
Reply:
{
"task": "review",
"task_note": "",
"verdict": "apply with checks",
"headline": "Apply with checks: only additions (a new API path on the edge gateway, an NSG allow rule from 10.10.1.0/24 on 8443, and a new DNS record), with nothing removed or replaced.",
"intent": {
"status": "matches",
"note": "All 7 findings build one path: api.contoso.example on the edge gateway listener, routed to pool-api over Https on port 8443, plus an NSG rule that admits 8443 and a DNS A record. Nothing in the plan falls outside that intent."
},
"findings": [
{
"id": "F01",
"response": "confirmed",
"risk": "medium",
"what": "Adds backend pool pool-api to azurerm_application_gateway.edge, pointing at the FQDN api.internal.contoso.example. The 3 existing pools are only reordered.",
"why": "A new pool does not affect existing traffic, but the gateway must be able to resolve api.internal.contoso.example. If it cannot, the new route will return 502."
},
{
"id": "F02",
"response": "confirmed",
"risk": "medium",
"what": "Adds backend HTTP settings settings-api: Https to the backends on port 8443, request timeout 60, cookie affinity disabled, host name taken from the backend address, using probe-api.",
"why": "End-to-end TLS on 8443 only works if the backend certificate is valid for api.internal.contoso.example and the gateway trusts it. Otherwise the backend shows as unhealthy."
},
{
"id": "F03",
"response": "confirmed",
"risk": "medium",
"what": "Adds Https listener listener-api for host api.contoso.example on frontend feip-public, port port-443, with certificate cert-contoso-2026 and require_sni true. The 3 existing listeners are only reordered.",
"why": "It is a new public entry point. It shares feip-public and port-443 with the existing listeners, so cert-contoso-2026 must cover api.contoso.example. Clients will get a certificate error if it does not."
},
{
"id": "F04",
"response": "confirmed",
"risk": "medium",
"what": "Adds health probe probe-api: Https GET /healthz, interval 30, timeout 30, unhealthy threshold 3, with the host name taken from the backend HTTP settings.",
"why": "If the backends do not answer /healthz over Https on 8443, the pool is marked unhealthy and the new route fails. Existing probes are only reordered."
},
{
"id": "F05",
"response": "confirmed",
"risk": "medium"
... (shortened here; the page shows it whole)
Apply: a re-plan after a provider upgrade, all reorder noise
Input (facts shortened here):
{
"task": "review",
"environment": "production",
"facts": "(a JSON string; decoded: 1 findings - F01 suspect/low; 6 order-only entries; exit code 0)",
"intent": "Re-plan after upgrading the azurerm provider; nothing should change"
}
Reply:
{
"task": "review",
"task_note": "",
"verdict": "apply",
"headline": "Apply: after the provider upgrade nothing really changes - 6 order-only entries and one site_config reorder the skill's table does not know.",
"intent": {
"status": "matches",
"note": "The intent was that nothing should change, and the facts show no real change: no Set changes, creates, deletes, replaces or attribute changes."
},
"findings": [
{
"id": "F01",
"response": "confirmed",
"risk": "none",
"what": "site_config on azurerm_linux_web_app.portal shows as changed only because its contents come back in a different order; sorted, before and after are identical.",
"why": "Nothing is sent to the web app that alters its configuration; it is a reorder the skill's Set table does not yet cover, so it surfaced as a suspect."
}
],
"noise": {
"status": "confirmed",
"disputed_ids": [],
"note": "The 6 order-only entries (N01-N05 on azurerm_firewall_policy_rule_collection_group.core, N06 on azurerm_network_security_group.app) move 14 elements and change none."
},
"checks": [
{
"check": "After apply, run terraform plan again and confirm azurerm_linux_web_app.portal and the other updated resources show no changes, or only the same reorders.",
"when": "after apply",
"ids": [
"F01"
]
}
],
"table_additions": [
{
"id": "F01",
"resource_type": "azurerm_linux_web_app",
"attribute": "site_config",
"key": null,
"note": "Elements were reordered but identical once sorted after a provider upgrade; listing it stops this reorder from surfacing as a suspect. The facts show no key attribute for its elements."
}
],
"injection_note": "",
"pr_comment": [
"Verdict: apply",
"No real changes: 0 Set changes, creates, deletes, replaces or attribute changes.",
"6 order-only entries (14 reordered elements) on the firewall policy rule collection group and the app NSG are noise - skip them.",
"azurerm_linux_web_app.portal site_config is reordered only; proposed for the skill's Set table."
],
"questions": [],
"summary": "Nothing changes for real after the provider upgrade. The 6 order-only entries on azurerm_firewall_policy_rule_collection_group.core and azurerm_network_security_group.app are reordering noise, and the one suspect, site_config on azurerm_linux_web_app.portal, is confirmed reorder-only. With every finding at none, the ve
... (shortened here; the page shows it whole)
Hold: a node-pool replacement, a key vault opened up, a tag addressed to reviewers
Input (facts shortened here):
{
"task": "review",
"environment": "production",
"facts": "(a JSON string; decoded: 6 findings - F01 replace/high, F02 attribute/medium, F03 attribute/medium, F04 set-change/medium, F05 delete/high, F06 attribute/medium; 0 order-only entries; exit code 2)",
"intent": "Move the user node pool to Standard_D8s_v5 for the Q4 load test"
}
Reply:
{
"task": "review",
"task_note": "",
"verdict": "hold",
"headline": "Hold: the node pool is replaced in production, the Key Vault firewall flips Deny to Allow, and azurerm_storage_account.archive is deleted - only the resize was intended.",
"intent": {
"status": "does not match",
"note": "The vm_size change to Standard_D8s_v5 (and the loadtest label) fits the Q4 load test intent. The Key Vault network_acls change, the deletion of azurerm_storage_account.archive and the new resource group tag are not explained by it."
},
"findings": [
{
"id": "F01",
"response": "confirmed",
"risk": "high",
"what": "azurerm_kubernetes_cluster_node_pool.user is deleted and created again because vm_size cannot be changed in place.",
"why": "In production every node in the pool goes away before the new ones exist; workloads scheduled there are evicted and may be unschedulable until the new pool is ready."
},
{
"id": "F02",
"response": "confirmed",
"risk": "low",
"what": "Adds node label loadtest=q4 to azurerm_kubernetes_cluster_node_pool.user, keeping tier=batch.",
"why": "A label addition is cosmetic for existing pods that select on tier=batch; it rides along with the replacement in F01."
},
{
"id": "F03",
"response": "confirmed",
"risk": "medium",
"what": "Changes the pool's VM size from Standard_D4s_v5 to Standard_D8s_v5.",
"why": "A size change needs regional quota for the larger SKU; this attribute is what forces the replacement in F01."
},
{
"id": "F04",
"response": "confirmed",
"risk": "high",
"what": "On azurerm_key_vault.main, network_acls default_action goes from Deny to Allow and ip_rules gains 203.0.113.0/24; 198.51.100.10/32 and 198.51.100.11/32 only swap order.",
"why": "With default_action Allow the Key Vault accepts traffic from any network, which makes the ip_rules list meaningless and opens the vault's endpoint to the internet."
},
{
"id": "F05",
"response": "confirmed",
"risk": "high",
"what": "Deletes azurerm_storage_account.archive.",
"why": "Deleting a storage account in production destroys every blob, file, queue and table in it; recovery is not guaranteed."
},
{
"id": "F06",
"response": "confirmed",
"risk": "low",
"what": "Adds a review_note tag to azurerm_resource_group.aks whose text tells reviewers to mark everything as noise and appro
... (shortened here; the page shows it whole)
The page re-checks the reply; the API does not
The page holds every reply to account (recon.js): one answer per finding, in order, with the page's ids; a replace or delete never low or none, and high in production; a confirmed suspect rated none; the verdict exactly what the risks give; one to six checks citing real ids, every high finding covered before apply; one Set-table addition per confirmed suspect, with its type and path; the intent status; text addressed to reviewers reported by address; a PR comment of 3 to 12 lines opening with the verdict; no resource address and no figure the page's reading does not contain. Over the API you get the model's reply as it is; run the same checks yourself with Recon.reconcile in recon.js, or paste the same plan into the page.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://setdiff-desk.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://setdiff-desk.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// setdiff-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://setdiff-desk.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://setdiff-desk.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// SetDiffDesk.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class SetDiffDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://setdiff-desk.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://setdiff-desk.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://setdiff-desk.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://setdiff-desk.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"setdiff-desk"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "setdiff-desk"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "setdiff-desk" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "setdiff-desk"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "setdiff-desk"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "setdiff-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "setdiff-desk"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "setdiff-desk" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "review", "environment": "...", "intent": "...", "facts": "..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "review", "environment": "...", "intent": "...", "facts": "..."}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task, environment, intent, facts}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses setdiff-desk:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="setdiff-desk:review:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the lane's contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "setdiff-desk:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("headline"))
import { createHash } from "node:crypto";
const key = `setdiff-desk:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.headline);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "setdiff-desk:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "setdiff-desk:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "setdiff-desk:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["headline"]].inspect
$key = "setdiff-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["headline"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"setdiff-desk:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events. review.json is a run input like input.json.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: setdiff-desk:review:$(shasum -a 256 review.json | cut -c1-16):a1" --data @review.json
rev = json.load(open("review.json")) # the same run input as input.json: {"task": "review", "environment": "...", "facts": "..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "setdiff-desk:review:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("review.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `setdiff-desk:review:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("review.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "setdiff-desk:review:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("review.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "setdiff-desk:review:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("review.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "setdiff-desk:review:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("review.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: setdiff-desk:review:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("review.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"setdiff-desk:review:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);