Drive the model-card agent from your own code
Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS with optional streaming. Send the raw material of a trained model — a training config, an eval table, dataset notes, a half-written README, or any mixture — and get back a publishable Hugging Face model card: frontmatter, model details, intended and out-of-scope uses, training and evaluation, limitations, plus the ranked list of gaps that must be closed before it can go up. Examples in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#.
Basics
Base URL: https://api.skillsafe.ai/v1/app-api. Every request sends
Authorization: Bearer <token> and JSON bodies with
Content-Type: application/json. Responses are wrapped in an envelope:
{"data": …} on success, {"error": {"code", "message"}} on
failure. Runs execute the app's agent (model gpt-terra) and are billed in
SkillSafe credits to the calling token, with a worst-case hold up front and the actual cost
settled when the job finishes.
| Status | Meaning |
|---|---|
401 | Missing or expired token — create a new session. |
402 | Not enough credits — top up at skillsafe.ai/account/billing. |
403 | The token isn't allowed to do this. |
404 | Unknown job or record id. |
5xx | Transient platform error — retry with backoff. |
Browsers enforce CORS for this API, so run these examples from a server, script or terminal — not from another website's frontend. The mechanical card linter on the app's page (which frontmatter keys exist, which template headings exist, how many placeholders remain, how many metric claims name a dataset) is browser-side computation with no endpoint behind it; the API surface is the agent run documented below.
Step 0 — A tiny client
Every step below is one or two HTTP calls, so start with a small helper that adds the auth
header, sends JSON and unwraps the data envelope. The later steps reuse this
helper.
export API="https://api.skillsafe.ai/v1/app-api"
export SKILLSAFE_TOKEN="YOUR_TOKEN" # see step 1
# every call looks like:
# curl -s "$API/…" -H "Authorization: Bearer $SKILLSAFE_TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": …} envelope
import json, os, requests
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ["SKILLSAFE_TOKEN"] # see step 1
def api(method, path, body=None, **headers):
res = requests.request(method, API + path, json=body,
headers={"Authorization": f"Bearer {TOKEN}", **headers})
payload = res.json()
if not res.ok:
raise RuntimeError(payload.get("error", {}).get("message", res.reason))
return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 1 — read it from your environment in real code
async function api(method, path, body, extraHeaders = {}) {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const API = "https://api.skillsafe.ai/v1/app-api"
var token = os.Getenv("SKILLSAFE_TOKEN") // see step 1
func call(method, path string, body, out any) error {
var buf bytes.Buffer
if body != nil {
json.NewEncoder(&buf).Encode(body)
}
req, _ := http.NewRequest(method, API+path, &buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct {
Data json.RawMessage `json:"data"`
Error *struct{ Message string `json:"message"` } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if res.StatusCode >= 400 {
return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
}
if out == nil {
return nil
}
return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class SkillSafe {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see step 1
static final HttpClient HTTP = HttpClient.newHttpClient();
static String api(String method, String path, String jsonBody) throws Exception {
var req = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) throw new RuntimeException(res.body());
return res.body(); // envelope: {"data": …}
}
}
require "net/http"
require "json"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN") # see step 1
def api(method, path, body = nil)
uri = URI(API + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = body.to_json if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise (payload.dig("error", "message") || res.message) unless res.is_a?(Net::HTTPSuccess)
payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN"); // see step 1
function api(string $method, string $path, ?array $body = null): mixed {
global $TOKEN;
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$payload = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($payload["error"]["message"] ?? "HTTP $status");
}
return $payload["data"];
}
// .NET 8+
using System.Net.Http.Json;
using System.Text.Json;
static class SkillSafe
{
const string Api = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new();
static SkillSafe() =>
Http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see step 1
public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null)
{
var req = new HttpRequestMessage(method, Api + path);
if (body != null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!res.IsSuccessStatusCode)
throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
return json.GetProperty("data");
}
}
Step 1 — Get a token
For scripted use, the simplest reliable path is your personal token: open the
token page, sign in, and hit
"Copy shell export" — it puts export SKILLSAFE_TOKEN="…" on your
clipboard, which every example below reads. Treat the token like a password — it can
spend your credits. For fully headless scripts, POST /guest (below) mints a
guest token with no browser involved; guests can always call /me and
/estimate, but whether a guest can afford an actual run depends on the app's
daily sponsorship budget, so don't build on it.
curl -s -X POST "$API/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"model-card-desk"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "model-card-desk"})["token"]
const { token } = await api("POST", "/guest", { slug: "model-card-desk" });
var guest struct{ Token string `json:"token"` }
err := call("POST", "/guest", map[string]string{"slug": "model-card-desk"}, &guest)
String envelope = api("POST", "/guest", """
{"slug":"model-card-desk"}""");
// token is at data.token in the returned JSON
token = api("POST", "/guest", { slug: "model-card-desk" })["token"]
$token = api("POST", "/guest", ["slug" => "model-card-desk"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
new { slug = "model-card-desk" });
var token = guest.GetProperty("token").GetString();
Step 2 — Check who you are and your balance
Returns subject_type ("user" or "guest"),
subject_id and your credits balance. Check this before an
expensive run.
curl -s "$API/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN" | jq '.data'
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
SubjectType string `json:"subject_type"`
Credits int64 `json:"credits"`
}
err := call("GET", "/me", nil, &me)
String envelope = api("GET", "/me", null);
// data.subject_type, data.credits
me = api("GET", "/me")
puts "#{me["subject_type"]}: #{me["credits"]} credits"
$me = api("GET", "/me");
echo "{$me['subject_type']}: {$me['credits']} credits\n";
var me = await SkillSafe.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")}: {me.GetProperty("credits")} credits");
Step 3 — Estimate the cost
Send the same input you would send to a run; the response's hold_credits is
the worst-case cost and min_credits the floor. Nothing is charged and no job
is created. The response also reports the resolved model and whether
sponsorship is active. The app itself refuses to start a run when
hold_credits exceeds the caller's balance — a sensible check for your
scripts too.
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
-d '{"card":"# tabular-churn-v2\nLightGBM, 48k rows of monthly billing history.\naccuracy 0.91, AUC 0.88\n…","notes":"internal retention team; Apache-2.0"}' | jq '.data'
est = api("POST", "/estimate", {"card": card, "notes": notes})
print("worst case:", est["hold_credits"], "credits on", est["model"])
const est = await api("POST", "/estimate", { card, notes });
console.log("worst case:", est.hold_credits, "credits on", est.model);
var est struct {
HoldCredits int64 `json:"hold_credits"`
Model string `json:"model"`
}
err := call("POST", "/estimate", map[string]string{
"card": card, "notes": notes,
}, &est)
String envelope = api("POST", "/estimate", """
{"card": %s, "notes": %s}
""".formatted(toJsonString(card), toJsonString(notes)));
// worst-case cost is at data.hold_credits
est = api("POST", "/estimate", { card: card, notes: notes })
puts "worst case: #{est["hold_credits"]} credits on #{est["model"]}"
$est = api("POST", "/estimate", [
"card" => $card,
"notes" => $notes,
]);
echo "worst case: {$est['hold_credits']} credits on {$est['model']}\n";
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", new {
card, notes });
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits");
Step 4 — Run it and wait
/run places a credit hold and returns a job_id; poll
/jobs/{job_id} every 1–2 seconds until status is
succeeded or failed. Always send an Idempotency-Key
header so a network retry can't start a second, double-charged run. The agent replies with
plain text in a fixed shape (see "The card's shape" below), delivered at
output.output.
| Input field | Type | Notes |
|---|---|---|
card | string, required | The raw material as pasted: YAML frontmatter, a partial README, a training_args dump, an eval table, bullet notes, or any mixture. Keep whole lines — never cut a config or a metric row in half. If you clip a long paste, keep the head and the tail and put a marker line such as [... 4200 characters omitted - material truncated ...] between them; the web app does exactly that at ~60k chars, and the agent will say in the summary that the material was sampled. |
notes | string, optional | Context: the audience for the card, licence constraints, where the model will be deployed, what you are worried about. Notes count as material, but anything sourced only from them is flagged as submitter-stated in ## Gaps. |
focus | string, optional | One of Auto detect (default), Full card from scratch, Review an existing card, Evaluation and metrics, Bias, risks and limitations, Frontmatter and metadata. It weights the depth of the work; all eight sections come back either way. |
lint | string, optional | A mechanical lint of the material (which frontmatter keys exist, which template headings exist, the placeholder count, how many metric claims name a dataset or split). The web app computes one in the browser; scripts can simply omit it — the agent reads the material itself either way. |
retry_note | string, optional | Feedback about a previous malformed reply; the agent obeys it exactly. |
$model | string, optional | Per-run model override (allowlisted models only). |
# input.json: {"card":"# tabular-churn-v2\nLightGBM…","focus":"Review an existing card"}
JOB_ID=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: card-$(date +%s)" \
-d @input.json | jq -r '.data.job_id')
while :; do
JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $SKILLSAFE_TOKEN")
STATUS=$(echo "$JOB" | jq -r '.data.status')
[ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
sleep 2
done
# the card is plain text at data.output.output
echo "$JOB" | jq -r '.data.output.output'
import time
card = open("README.draft.md").read() + "\n" + open("training_args.json").read()
notes = "internal retention team; Apache-2.0; scored monthly"
job_id = api("POST", "/run", {
"card": card,
"notes": notes,
"focus": "Review an existing card",
}, **{"Idempotency-Key": "card-001"})["job_id"]
while True:
job = api("GET", f"/jobs/{job_id}")
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1.5)
if job["status"] == "failed":
raise RuntimeError(job.get("error", "run failed"))
raw = job["output"]
report = raw["output"] if isinstance(raw, dict) else raw # plain text, not JSON
print(report.splitlines()[0]) # e.g. "VERDICT: Needs disclosure"
const { job_id } = await api("POST", "/run", {
card,
notes,
focus: "Review an existing card",
}, { "Idempotency-Key": crypto.randomUUID() });
let job;
do {
await new Promise((r) => setTimeout(r, 1500));
job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");
if (job.status === "failed") throw new Error(job.error ?? "run failed");
const report = job.output?.output ?? job.output; // plain text, not JSON
console.log(report.split("\n")[0]); // e.g. "VERDICT: Needs disclosure"
var started struct{ JobID string `json:"job_id"` }
err := call("POST", "/run", map[string]string{
"card": card, "notes": notes, "focus": "Review an existing card",
}, &started)
if err != nil {
log.Fatal(err)
}
var job struct {
Status string `json:"status"`
Error string `json:"error"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
for {
if err := call("GET", "/jobs/"+started.JobID, nil, &job); err != nil {
log.Fatal(err)
}
if job.Status == "succeeded" || job.Status == "failed" {
break
}
time.Sleep(1500 * time.Millisecond)
}
report := job.Output.Output // plain text, not JSON
fmt.Println(strings.SplitN(report, "\n", 2)[0])
String envelope = api("POST", "/run", """
{"card": %s, "notes": %s, "focus": "Review an existing card"}
""".formatted(toJsonString(card), toJsonString(notes)));
String jobId = /* data.job_id via your JSON library */;
while (true) {
String job = api("GET", "/jobs/" + jobId, null);
String status = /* data.status */;
if (status.equals("succeeded") || status.equals("failed")) break;
Thread.sleep(1500);
}
// the card is the plain-text STRING at data.output.output — no second
// JSON parse needed, just read the string field
started = api("POST", "/run", { card: card, notes: notes,
focus: "Review an existing card" })
job = nil
loop do
job = api("GET", "/jobs/#{started["job_id"]}")
break if %w[succeeded failed].include?(job["status"])
sleep 1.5
end
raise (job["error"] || "run failed") if job["status"] == "failed"
raw = job["output"]
report = raw.is_a?(Hash) ? raw["output"] : raw # plain text, not JSON
puts report.lines.first # e.g. "VERDICT: Needs disclosure"
$started = api("POST", "/run", [
"card" => $card,
"notes" => $notes,
"focus" => "Review an existing card",
]);
do {
sleep(2);
$job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"]));
if ($job["status"] === "failed") {
throw new Exception($job["error"] ?? "run failed");
}
$raw = $job["output"];
$report = is_array($raw) ? ($raw["output"] ?? "") : $raw; // plain text, not JSON
echo strtok($report, "\n") . "\n"; // e.g. "VERDICT: Needs disclosure"
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", new {
card, notes, focus = "Review an existing card" });
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
var status = job.GetProperty("status").GetString();
if (status is "succeeded" or "failed") break;
await Task.Delay(1500);
}
var report = job.GetProperty("output").GetProperty("output").GetString()!;
Console.WriteLine(report.Split('\n')[0]); // e.g. "VERDICT: Needs disclosure"
The card's shape
The reply is plain text (markdown bullets, no code fence around the whole thing) in exactly this frame — stable enough to parse with a few string splits:
VERDICT: Publish ready | Needs disclosure | Not publishable yet | Insufficient material
MODEL: <hub-style model id, e.g. acme/tabular-churn-v2 - or Unknown>
COMPLETENESS: <integer 0-100>
SUMMARY: <2-4 sentences, may wrap, ends at the first blank line>
## Frontmatter
- <one YAML line per bullet, exactly `key: value`; lists inline, e.g. tags: [tabular, churn]>
## Model details
- <plain bullets: architecture or base model, parameter count, training date, author, licence>
## Intended uses
- <plain bullets: concrete tasks, each naming the user and the situation>
## Out of scope uses
- <plain bullets: uses the material gives reason to warn against, and why>
## Training and evaluation
- <plain bullets: data, procedure, and every metric as `metric value on dataset split`>
## Bias, risks and limitations
- <plain bullets: tied to the class balance, the collection window, the eval set's size>
## Gaps
- <gap> | <Blocker|Should fix|Nice to have> | <what the material says or fails to say> | <what to fetch>
## Unsupported claims
- <plain bullets: what a reader would take away that the material does not prove>
- All four header lines and all eight
##sections always appear, in that order. A section with nothing to report is the single bullet- None. - Every
## Frontmatterbullet is exactlykey: value— split on the first:to build the YAML block. Only keys the Hub actually reads appear (license,language,library_name,pipeline_tag,tags,datasets,metrics,base_model,model_name,finetuned_from); anything else is worth flagging, since the Hub ignores unknown keys silently. - Every
## Gapsbullet has exactly four|-separated fields — the gap, the severity, what the material says, what to fetch. Split on" | "; severity is exactlyBlocker,Should fixorNice to have. - Cross-rules worth asserting client-side:
Publish readyimplies noBlockergap;Not publishable yetandInsufficient materialeach imply at least oneBlocker;Insufficient materialimpliesCOMPLETENESSbelow 40. When a reply breaks one, trust the gaps over the verdict. - Template placeholders (
[More Information Needed],{{…}},TODO) are forbidden in the output — if one arrives, the material had a hole the agent should have written as a gap instead. - Model ids, frontmatter keys, metric names, dataset names and quoted values arrive wrapped in backticks (
`accuracy`,`0.91`,`acme/tabular-churn-v2`). ## Gapsand## Unsupported claimsare the work list, not part of the published card — assemble the README from the frontmatter pairs plus the five prose sections, and keep the other two beside it.
If a reply ever fails to match the frame, retry once with the same input plus a
retry_note field describing the problem — the agent is instructed to
obey it. That's exactly what the app itself does (and the retry is a second billed run).
Step 5 — The same run, streamed
Identical input to /run, but the response is
text/event-stream, so you can show the card as it generates (the app's live
output panel is this endpoint). Events:
| Event | Data |
|---|---|
job | {job_id} — the run was accepted. |
delta | {text} — the next chunk of agent output. |
done / pending | Final payload: {job_id, status, charged_credits, output}. Authoritative — deltas can drop the tail, so always read the final card from here. |
error | {code, message, job_id}. |
curl -sN -X POST "$API/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
-d @input.json
# event: job data: {"job_id":"job_…"}
# event: delta data: {"text":"VERDICT: Needs disclosure\nMODEL:"}
# …
# event: done data: {"job_id":"…","status":"succeeded","charged_credits":412,
# "output":{"output":"…the full card text…"}}
res = requests.post(API + "/run-stream", json=payload, stream=True,
headers={"Authorization": f"Bearer {TOKEN}"})
event, done = None, None
for line in res.iter_lines(decode_unicode=True):
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data = json.loads(line[5:])
if event == "delta":
print(data.get("text", ""), end="", flush=True)
elif event in ("done", "pending"):
done = data
elif event == "error":
raise RuntimeError(data.get("message"))
report = done["output"]["output"] # authoritative full text
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", event = "message", done, out = "";
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buf += decoder.decode(chunk.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(6).trim();
else if (line.startsWith("data:")) {
const data = JSON.parse(line.slice(5));
if (event === "delta") out += data.text ?? "";
else if (event === "done" || event === "pending") done = data;
else if (event === "error") throw new Error(data.message);
}
}
}
const report = done.output.output; // authoritative full text
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 1<<20), 1<<20)
event, done := "", []byte(nil)
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "event:") {
event = strings.TrimSpace(line[6:])
} else if strings.HasPrefix(line, "data:") {
data := strings.TrimSpace(line[5:])
if event == "delta" {
// unmarshal {"text": …} and append
} else if event == "done" || event == "pending" {
done = []byte(data)
}
}
}
// unmarshal done → .output.output (the full plain-text card)
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payloadJson))
.build();
var lines = HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body();
final String[] event = {""};
StringBuilder doneData = new StringBuilder();
lines.forEach(line -> {
if (line.startsWith("event:")) event[0] = line.substring(6).trim();
else if (line.startsWith("data:")) {
if (event[0].equals("delta")) { /* parse {"text"} and append */ }
else if (event[0].equals("done")) doneData.append(line.substring(5).trim());
}
});
// parse doneData → output.output (the full plain-text card)
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = payload.to_json
event, done, buf = nil, nil, ""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
buf << chunk
while (i = buf.index("\n"))
line = buf.slice!(0..i).chomp
if line.start_with?("event:") then event = line[6..].strip
elsif line.start_with?("data:")
data = JSON.parse(line[5..])
print data["text"] if event == "delta"
done = data if %w[done pending].include?(event)
end
end
end
end
end
report = done["output"]["output"] # authoritative full text
$event = ""; $done = null; $buf = "";
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $TOKEN", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$done, &$buf) {
$buf .= $chunk;
while (($i = strpos($buf, "\n")) !== false) {
$line = rtrim(substr($buf, 0, $i)); $buf = substr($buf, $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") echo $data["text"] ?? "";
if ($event === "done" || $event === "pending") $done = $data;
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$report = $done["output"]["output"]; // authoritative full text
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream")
{ Content = JsonContent.Create(payload) };
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string? line; string ev = ""; JsonElement doneEl = default;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:"))
{
var data = JsonDocument.Parse(line[5..]).RootElement.Clone();
if (ev == "delta") Console.Write(
data.TryGetProperty("text", out var t) ? t.GetString() : "");
else if (ev is "done" or "pending") doneEl = data;
}
}
var report = doneEl.GetProperty("output").GetProperty("output").GetString()!;
The grounding contract is instructed on every run: nothing enters the card that is not in your card material or your notes - no invented licence, base model, parameter count, training date or dataset name; every metric carries its dataset and split, or is written with the missing part named rather than dressed up; template placeholders are dropped and rewritten as gaps; anything sourced only from your notes is marked as submitter-stated. If you clip a long paste, keep the head and the tail with a marker line and the card will scope its claims to what it actually saw. Note that this is a contract the prompt imposes on the model, not a server-side guarantee: the web app additionally checks every backticked citation back against the submitted card and marks the ones it cannot find, and an API client that cares about grounding should do the same on the fields it consumes - especially any metric or licence it plans to publish.