Driving Draft Desk from your own code
Everything the page does over the network, you can do from a script: mint a token, price a run for free, then write the article — either as a job you poll, or as a stream you read as it is written. Pick your language once and every example on the page follows.
Base URL https://api.skillsafe.ai/v1/app-api
There is no /apps/{slug}/ segment — the slug is bound to the
token when you mint it at /guest. Every response is the envelope
{"ok":true,"data":{…}} or {"ok":false,"error":{…}}.
The /run body is the input object directly, not
{"input":{…}}.
The input Draft Desk accepts
| Field | Required | What it is |
|---|---|---|
material | yes | The raw material as pasted: notes, bullets, a transcript, research fragments, or an existing draft. The page sends at most 60,000 characters, keeping both ends and dropping the middle behind a marker. |
voice | no | How the author writes — sample paragraphs beat a description. Empty means the voice is inferred from the material. |
audience | no | Who reads it, where it runs, what it should achieve. |
format | no | Auto, Technical guide, Essay, Newsletter or Blog post. Defaults to Auto. |
mode | no | Auto, Write from notes or Tighten an existing draft. |
facts | no | A one-line summary of a mechanical scan of the material (counts, banned-pattern matches, TBD markers). It is a hint the model is told to verify, never a fact. |
retry_note | no | Only sent on the automatic reformat retry: a verbatim restatement of the output shape, used when a first reply did not parse. Leave it out of a normal run. |
The output contract
data.output.output is plain text, and the page rejects anything that does not match
this shape exactly — five tag lines, then four sections in order:
TITLE: <plain text>
FORMAT: Technical guide | Essay | Newsletter | Blog post
VERDICT: Ready to publish | Needs your facts | Not enough material
CONFIDENCE: <integer 0-100>
SUMMARY: <2-4 sentences, ends at the first blank line>
## Article
<the article in markdown, ### headings or deeper only>
## Voice notes
- <at least one bullet>
## What was cut
- <bullets, or the single bullet "Nothing cut.">
## Missing facts
- <bullets, or the single bullet "Nothing missing.">
Facts the material did not carry appear in the article as <placeholder> tokens
in angle brackets and as bullets under Missing facts — never as invented values. A
Ready to publish verdict alongside real Missing facts bullets is a contradiction the
page flags rather than hides.
Errors
| HTTP | error.code | What to do |
|---|---|---|
| 401 | unauthorized | The token is missing, expired or revoked. Mint a new one at /guest, or sign in on the token page. |
| 402 | payment_required | The balance will not cover the reserve. Check /estimate against /me first — that is what makes this unreachable. |
| 404 | not_found | Almost always a wrong path: there is no /apps/{slug}/ segment on these routes. |
| 422 | validation_error | The body was not the input object, or material was empty. |
| 429 | rate_limited | Back off and retry with the same Idempotency-Key. |
| 5xx | internal | Retry with the same Idempotency-Key; a replay returns deduped:true and the original job rather than billing again. |
1Get a token
A guest token is enough for /me and the free /estimate. For metered runs
billed to your account, sign in on the token page and copy the shell
export from there — you never need the browser console.
# A guest token, no browser involved. Guests can call /me and the free
# /estimate; sign in on https://draft-desk.skillsafe.ai/tokens.html for a
# personal token so metered runs bill your account.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"draft-desk"}'
# {"ok":true,"data":{"token":"aut_...","guest_id":"gst_...","expires_at":"..."}}
# Keep it in the environment, never in the source:
export SKILLSAFE_TOKEN="aut_..."
import os, json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None, token=None, stream=False):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
if token:
req.add_header("Authorization", "Bearer " + token)
resp = urllib.request.urlopen(req)
if stream:
return resp
payload = json.loads(resp.read())
if not payload.get("ok"):
raise RuntimeError(payload.get("error"))
return payload["data"]
token = os.environ.get("SKILLSAFE_TOKEN")
if not token:
token = call("POST", "/guest", {"slug": "draft-desk"})["token"]
print(token)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let token = "YOUR_TOKEN"; // read it from your own config/secret store
async function call(method, path, body, opts = {}) {
const headers = { "Content-Type": "application/json" };
if (token) headers.Authorization = `Bearer ${token}`;
if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
const res = await fetch(BASE + path, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
if (opts.raw) return res;
const payload = await res.json();
if (!payload.ok) throw new Error(JSON.stringify(payload.error));
return payload.data;
}
if (!token) token = (await call("POST", "/guest", { slug: "draft-desk" })).token;
console.log(token);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const Base = "https://api.skillsafe.ai/v1/app-api"
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error json.RawMessage `json:"error"`
}
func call(method, path string, body any, token string) (json.RawMessage, error) {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, Base+path, rdr)
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s", e.Error)
}
return e.Data, nil
}
func main() {
token := os.Getenv("SKILLSAFE_TOKEN")
if token == "" {
raw, err := call("POST", "/guest", map[string]string{"slug": "draft-desk"}, "")
if err != nil {
panic(err)
}
var g struct {
Token string `json:"token"`
}
json.Unmarshal(raw, &g)
token = g.Token
}
fmt.Println(token)
}
import java.net.URI;
import java.net.http.*;
import java.util.*;
public class DraftDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String token = System.getenv("SKILLSAFE_TOKEN");
static String call(String method, String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Content-Type", "application/json");
if (token != null) b.header("Authorization", "Bearer " + token);
b.method(method, jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody));
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
return res.body(); // {"ok":true,"data":{...}} - decode with your JSON library
}
public static void main(String[] args) throws Exception {
if (token == null) {
System.out.println(call("POST", "/guest", "{\"slug\":\"draft-desk\"}"));
}
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
$token = ENV["SKILLSAFE_TOKEN"]
def call(method, path, body = nil, headers = {})
uri = URI(BASE + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.fetch(method)
req = klass.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{$token}" if $token
headers.each { |k, v| req[k] = v }
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise payload["error"].to_s unless payload["ok"]
payload["data"]
end
$token ||= call("POST", "/guest", { "slug" => "draft-desk" })["token"]
puts $token
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: null;
function call(string $method, string $path, $body = null, array $extra = []) {
global $token;
$headers = ["Content-Type: application/json"];
if ($token) $headers[] = "Authorization: Bearer $token";
foreach ($extra as $k => $v) $headers[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) throw new RuntimeException(json_encode($payload["error"] ?? null));
return $payload["data"];
}
if (!$token) $token = call("POST", "/guest", ["slug" => "draft-desk"])["token"];
echo $token, PHP_EOL;
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class DraftDesk {
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new HttpClient();
static string token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN");
static async Task<JsonElement> Call(HttpMethod method, string path, object body = null,
string idempotencyKey = null) {
var req = new HttpRequestMessage(method, Base + path);
if (body != null)
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
if (token != null) req.Headers.Add("Authorization", "Bearer " + token);
if (idempotencyKey != null) req.Headers.Add("Idempotency-Key", idempotencyKey);
var res = await Http.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").ToString());
return doc.RootElement.GetProperty("data");
}
static async Task Main() {
if (token == null)
token = (await Call(HttpMethod.Post, "/guest", new { slug = "draft-desk" })).GetProperty("token").GetString();
Console.WriteLine(token);
}
}
2Check who you are and what you have
curl -s "https://api.skillsafe.ai/v1/app-api/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# {"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":48213}}
# subject_type is "guest" for a guest token. credits are in hundred-thousandths
# of a dollar: 10000 credits = $1.00.
me = call("GET", "/me", token=token)
print(me["subject_type"], me["credits"])
const me = await call("GET", "/me");
console.log(me.subject_type, me.credits);
raw, err := call("GET", "/me", nil, token)
if err != nil {
panic(err)
}
fmt.Println(string(raw))
System.out.println(call("GET", "/me", null));
me = call("GET", "/me")
puts "#{me["subject_type"]} #{me["credits"]}"
$me = call("GET", "/me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var me = await Call(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
3Price it — free, and it proves the model binding
/estimate creates no job and charges nothing. It is also the authoritative check that
the app is wired to the tier you expect: model_alias reads gpt-terra
and markup_bps is 1000. The app is bound to the alias, not to
a concrete model id, so read model from this response rather than hard-coding it —
an alias repoints as new models ship (it resolves to gpt-5.6-terra today) and can
only ever move to a model at or below the outgoing one’s token rates.
# Free: creates no job and charges nothing. This is also the call that
# proves which model the app is bound to.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"material": "Migration notes...\\nPostgres GIN full text -> SQLite FTS5. p95 412ms -> 38ms. Index 1.8GB -> 210MB.\\nPrice: CHECK with Sam, do not guess.",
"voice": "Short sentences. Dry. No exclamation marks.",
"audience": "Backend engineers on our engineering blog.",
"format": "Technical guide",
"mode": "Write from notes",
"facts": "Mechanical scan of the material (pattern matching, verify before repeating): 78 words, 9 numeric tokens, 0 URLs, 0 quoted passages, marked gaps: line 3 \\\"CHECK with Sam\\\"."
}'
# {"ok":true,"data":{"hold_credits":1561,"min_credits":133,
# "model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"sponsor_enabled":false,"byok":false}}
#
# hold_credits is RESERVED, not charged - it prices the full output cap. The
# settled charge is usually far lower. Compare hold_credits against the balance
# from /me before you run, exactly as the page does.
payload = {
"material": "Migration notes...\\nPostgres GIN full text -> SQLite FTS5. p95 412ms -> 38ms. Index 1.8GB -> 210MB.\\nPrice: CHECK with Sam, do not guess.",
"voice": "Short sentences. Dry. No exclamation marks.",
"audience": "Backend engineers on our engineering blog.",
"format": "Technical guide",
"mode": "Write from notes",
"facts": "Mechanical scan of the material (pattern matching, verify before repeating): 78 words, 9 numeric tokens, 0 URLs, 0 quoted passages, marked gaps: line 3 \\\"CHECK with Sam\\\"."
}
est = call("POST", "/estimate", payload, token=token)
print(est["model"], est["model_alias"], est["markup_bps"])
print("reserves", est["hold_credits"], "minimum", est["min_credits"])
if me["credits"] < est["min_credits"]:
raise SystemExit("top up before running")
const payload = {
"material": "Migration notes...\\nPostgres GIN full text -> SQLite FTS5. p95 412ms -> 38ms. Index 1.8GB -> 210MB.\\nPrice: CHECK with Sam, do not guess.",
"voice": "Short sentences. Dry. No exclamation marks.",
"audience": "Backend engineers on our engineering blog.",
"format": "Technical guide",
"mode": "Write from notes",
"facts": "Mechanical scan of the material (pattern matching, verify before repeating): 78 words, 9 numeric tokens, 0 URLs, 0 quoted passages, marked gaps: line 3 \\\"CHECK with Sam\\\"."
};
const est = await call("POST", "/estimate", payload);
console.log(est.model, est.model_alias, est.markup_bps);
if (me.credits < est.min_credits) throw new Error("top up before running");
payload := map[string]any{
"material": "Migration notes...",
"voice": "Short sentences. Dry. No exclamation marks.",
"audience": "Backend engineers on our engineering blog.",
"format": "Technical guide",
"mode": "Write from notes",
}
raw, err = call("POST", "/estimate", payload, token)
if err != nil {
panic(err)
}
fmt.Println(string(raw))
String payload = """
{"material":"Migration notes...","voice":"Short sentences. Dry.",
"audience":"Backend engineers on our engineering blog.",
"format":"Technical guide","mode":"Write from notes"}
""";
System.out.println(call("POST", "/estimate", payload));
payload = {
"material" => "Migration notes...",
"voice" => "Short sentences. Dry. No exclamation marks.",
"audience" => "Backend engineers on our engineering blog.",
"format" => "Technical guide",
"mode" => "Write from notes"
}
est = call("POST", "/estimate", payload)
puts "#{est["model"]} #{est["model_alias"]} #{est["markup_bps"]}"
$payload = [
"material" => "Migration notes...",
"voice" => "Short sentences. Dry. No exclamation marks.",
"audience" => "Backend engineers on our engineering blog.",
"format" => "Technical guide",
"mode" => "Write from notes",
];
$est = call("POST", "/estimate", $payload);
echo $est["model"], " ", $est["model_alias"], " ", $est["markup_bps"], PHP_EOL;
var payload = new {
material = "Migration notes...",
voice = "Short sentences. Dry. No exclamation marks.",
audience = "Backend engineers on our engineering blog.",
format = "Technical guide",
mode = "Write from notes"
};
var est = await Call(HttpMethod.Post, "/estimate", payload);
Console.WriteLine($"{est.GetProperty("model")} {est.GetProperty("markup_bps")}");
4Write the article — job and poll
This one is metered. Send an Idempotency-Key on every run: a retried request replays
the same job instead of billing a second one. Derive it from a hash of the input plus a counter you
bump for each deliberate new run — a key that only advances on success will replay a failed
attempt's job when you try again.
# METERED. The body is the input object DIRECTLY - not {"input": {...}}.
# Idempotency-Key makes a retried request replay one job instead of billing two.
KEY="draft-desk:$(printf '%s' "$MATERIAL" | shasum -a 256 | cut -c1-16)-g1"
JOB=$(curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @input.json | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
# Poll to a terminal state.
until curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
| tee /tmp/job.json | grep -q '"status":"succeeded"\|"status":"failed"'; do
sleep 2
done
python3 -c 'import json;print(json.load(open("/tmp/job.json"))["data"]["output"]["output"])'
import time, hashlib
key = "draft-desk:" + hashlib.sha256(payload["material"].encode()).hexdigest()[:16] + "-g1"
req = urllib.request.Request(BASE + "/run", data=json.dumps(payload).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + token)
req.add_header("Idempotency-Key", key) # a replay returns deduped:true
job = json.loads(urllib.request.urlopen(req).read())["data"]
while True:
j = call("GET", "/jobs/" + job["job_id"], token=token)
if j["status"] in ("succeeded", "failed"):
break
time.sleep(2)
print(j["output"]["output"]) # the TITLE:/FORMAT:/... reply
print("charged", j.get("charged_credits"), "truncated", j.get("truncated"))
import { createHash } from "node:crypto";
const key = `draft-desk:${createHash("sha256").update(payload.material).digest("hex").slice(0, 16)}-g1`;
const job = await call("POST", "/run", payload, { idempotencyKey: key });
let j;
do {
await new Promise((r) => setTimeout(r, 2000));
j = await call("GET", `/jobs/${job.job_id}`);
} while (j.status !== "succeeded" && j.status !== "failed");
console.log(j.output.output);
console.log("charged", j.charged_credits, "truncated", j.truncated);
// Add the Idempotency-Key header to the request in call() when you need it.
raw, err = call("POST", "/run", payload, token)
if err != nil {
panic(err)
}
var job struct {
JobID string `json:"job_id"`
}
json.Unmarshal(raw, &job)
for {
raw, err = call("GET", "/jobs/"+job.JobID, nil, token)
if err != nil {
panic(err)
}
var j struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
json.Unmarshal(raw, &j)
if j.Status == "succeeded" || j.Status == "failed" {
fmt.Println(j.Output.Output)
break
}
time.Sleep(2 * time.Second)
}
String job = call("POST", "/run", payload); // add Idempotency-Key in call()
// Extract job_id with your JSON library, then poll:
// GET /jobs/{job_id} until status is "succeeded" or "failed",
// then read data.output.output for the TITLE:/FORMAT:/... reply.
require "digest"
key = "draft-desk:#{Digest::SHA256.hexdigest(payload["material"])[0, 16]}-g1"
job = call("POST", "/run", payload, { "Idempotency-Key" => key })
loop do
j = call("GET", "/jobs/#{job["job_id"]}")
if %w[succeeded failed].include?(j["status"])
puts j["output"]["output"]
break
end
sleep 2
end
$key = "draft-desk:" . substr(hash("sha256", $payload["material"]), 0, 16) . "-g1";
$job = call("POST", "/run", $payload, ["Idempotency-Key" => $key]);
while (true) {
$j = call("GET", "/jobs/" . $job["job_id"]);
if (in_array($j["status"], ["succeeded", "failed"], true)) {
echo $j["output"]["output"], PHP_EOL;
break;
}
sleep(2);
}
using System.Security.Cryptography;
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(payload.material)))
.ToLowerInvariant()[..16];
var job = await Call(HttpMethod.Post, "/run", payload, $"draft-desk:{hash}-g1");
var jobId = job.GetProperty("job_id").GetString();
JsonElement j;
do {
await Task.Delay(2000);
j = await Call(HttpMethod.Get, $"/jobs/{jobId}");
} while (j.GetProperty("status").GetString() is not ("succeeded" or "failed"));
Console.WriteLine(j.GetProperty("output").GetProperty("output").GetString());
5Or stream it
/run-stream is the same run over server-sent events, which is what the page uses so
the article appears as it is written. Read event: delta for the text as it arrives,
but take event: done as authoritative — the delta stream can drop the tail.
done also carries charged_credits (the real cost, usually well under the
reserve) and truncated.
# METERED. Server-sent events: the article arrives as it is written.
curl -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @input.json
# event: job data: {"job_id":"job_..."}
# event: delta data: {"text":"TITLE: We moved off Postgres"}
# event: delta data: {"text":" full text search\n"}
# event: done data: {"output":{"output":"TITLE: ..."},"charged_credits":740,
# "truncated":false}
#
# The done payload is authoritative - the deltas can drop the tail.
resp = call("POST", "/run-stream", payload, token=token, stream=True)
full = ""
event = None
for line in resp:
line = line.decode().rstrip("\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
chunk = json.loads(line[5:].strip())
if event == "delta":
full += chunk["text"]
elif event == "done":
full = chunk["output"]["output"] # authoritative
print("charged", chunk.get("charged_credits"))
print(full)
const res = await call("POST", "/run-stream", payload,
{ raw: true, idempotencyKey: key });
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", full = "", event = null;
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let nl;
while ((nl = buf.indexOf("\n")) !== -1) {
const line = buf.slice(0, nl).trim();
buf = buf.slice(nl + 1);
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) {
const d = JSON.parse(line.slice(5).trim());
if (event === "delta") full += d.text;
if (event === "done") full = d.output.output; // authoritative
}
}
}
console.log(full);
req, _ := http.NewRequest("POST", Base+"/run-stream", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", key)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var event, full string
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
var d struct {
Text string `json:"text"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &d)
if event == "delta" {
full += d.Text
} else if event == "done" {
full = d.Output.Output
}
}
}
fmt.Println(full)
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<java.util.stream.Stream<String>> res =
HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
StringBuilder full = new StringBuilder();
String[] event = { null };
res.body().forEach(line -> {
if (line.startsWith("event:")) event[0] = line.substring(6).trim();
else if (line.startsWith("data:") && "delta".equals(event[0])) {
// parse {"text":"..."} with your JSON library and append it
}
});
System.out.println(full);
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{$token}"
req["Idempotency-Key"] = key
req.body = JSON.dump(payload)
full = +""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.strip
if line.start_with?("event:")
event = line[6..].strip
elsif line.start_with?("data:")
d = JSON.parse(line[5..].strip)
full << d["text"].to_s if event == "delta"
full = d["output"]["output"] if event == "done"
end
end
end
end
end
puts full
$ch = curl_init(BASE . "/run-stream");
$full = "";
$event = null;
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Bearer $token",
"Idempotency-Key: $key",
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$full, &$event) {
foreach (explode("\n", $chunk) as $line) {
$line = trim($line);
if (str_starts_with($line, "event:")) {
$event = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:")) {
$d = json_decode(trim(substr($line, 5)), true);
if ($event === "delta") $full .= $d["text"] ?? "";
if ($event === "done") $full = $d["output"]["output"];
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
echo $full, PHP_EOL;
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream") {
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
req.Headers.Add("Authorization", "Bearer " + token);
req.Headers.Add("Idempotency-Key", key);
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var full = new StringBuilder();
string ev = null, line;
while ((line = await reader.ReadLineAsync()) != null) {
line = line.Trim();
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:")) {
var d = JsonDocument.Parse(line[5..].Trim()).RootElement;
if (ev == "delta") full.Append(d.GetProperty("text").GetString());
if (ev == "done") { full.Clear(); full.Append(d.GetProperty("output").GetProperty("output").GetString()); }
}
}
Console.WriteLine(full);
Doing the free lane yourself
The material scan, the banned-pattern catalog, the article re-lint and the grounding check all run
in the browser and need no API at all — they are in
draftlint.js, and the reply parser and renderers are in
report.js. If you are scripting this, the useful part is the grounding
check: pull the figures and URLs back out of data.output.output and assert every one
appears in the material you sent.