Drive Base File Fixer from your own code
Base File Fixer checks an Obsidian Bases .base file before you open it. In the page, and free,
a strict YAML reader gives line-accurate errors, the Bases file format is checked (filters, formulas,
properties, summaries, views), every filter and formula is parsed and given a type pass (Duration
fields before rounding, guarded date() calls, defined and non-circular formulas, exact
summary names), and the file is run over sample notes you paste to preview each view.
The metered lanes read what the page found: fix answers every finding and returns the
corrected file; build writes a new .base file from a description of what it
should show. The page runs any file in a reply back through the same checks and preview; from the
API, paste the returned file into the page to get the same re-check.
Two lanes: the task field
| task | send | you get back |
|---|---|---|
fix | base, facts; symptom optional | headline, verdict on the original file (broken, fix_first, ok), a stance on every finding (finding_responses), changes (where, before, after, why), fixed_yaml (the complete corrected file, or "" when nothing needs to change) and to_check. |
build | request, facts; base optional (a file to extend) | headline, base_yaml (the complete file), explanation (one entry per top-level part), properties_used, assumptions and to_check. |
If task is missing or unrecognised, the model picks the closer lane and says which in the
reply's task; always send it, and check the reply's task against it.
Worked example: fix
A small reading list with three findings from the page: formulas.days_reading calls .round() on a Duration (an error), groupBy.direction is lowercase (an error), and date(started) is not guarded against notes without started (a warning). No sample notes were pasted, so facts.notes and facts.preview are null. This is the exact body the page sends:
{
"task": "fix",
"base": "filters:\n and:\n - file.hasTag(\"book\")\nformulas:\n days_reading: (today() - date(started)).round(0)\nviews:\n - type: table\n name: Reading\n order:\n - file.name\n - author\n - formula.days_reading\n groupBy:\n property: status\n direction: asc\n",
"facts": "{\"checker\":\"Base File Fixer (YAML, Bases schema, expression parse and type checks, preview over pasted notes)\",\"status\":\"broken\",\"preview_clock\":\"2026-09-27 12:00\",\"base_path\":\"Reading.base\",\"findings\":[{\"id\":\"F1\",\"severity\":\"error\",\"source\":\"expr\",\"code\":\"duration-method\",\"where\":\"formulas.days_reading\",\"line\":5,\"text\":\"Subtracting dates gives a Duration, which has no .round(). Read a number first: (...).days.round(...) - the skill's own rule.\",\"quote\":\"(today() - date(started)).round(0)\"},{\"id\":\"F2\",\"severity\":\"error\",\"source\":\"schema\",\"code\":\"group-direction\",\"where\":\"views[0].groupBy.direction\",\"line\":15,\"text\":\"direction must be ASC or DESC (uppercase), not 'asc'.\",\"quote\":\"\"},{\"id\":\"F3\",\"severity\":\"warning\",\"source\":\"expr\",\"code\":\"unguarded-date\",\"where\":\"formulas.days_reading\",\"line\":5,\"text\":\"date(started) errors on any note without a started value (the skill's 'missing null check'). Guard it: if(started, ..., \\\"\\\").\",\"quote\":\"(today() - date(started)).round(0)\"}],\"findings_total\":3,\"formula_types\":{\"days_reading\":\"number\"},\"notes\":null,\"preview\":null,\"base_sent\":{\"lines\":16,\"lines_cut\":0}}",
"symptom": "The days_reading column shows an error on every row."
}
A reply in the lane's shape (illustrative; the page re-checked this fixed_yaml and it has no findings):
{
"task": "fix",
"headline": "formulas.days_reading called .round() on a Duration and read started without a guard, and the grouping direction was lowercase; the fixed file reads .days behind if(started, ...) and groups by status ASC.",
"verdict": "broken",
"finding_responses": [
{
"ref": "F1",
"stance": "fixed",
"note": "today() minus a date is a Duration, which has no .round(). The formula now reads .days, which is already a whole number."
},
{
"ref": "F2",
"stance": "fixed",
"note": "views[0].groupBy.direction is now ASC; only uppercase ASC or DESC is valid."
},
{
"ref": "F3",
"stance": "fixed",
"note": "The formula now returns \"\" on a note without started instead of an error."
}
],
"changes": [
{
"where": "formulas.days_reading",
"before": "days_reading: (today() - date(started)).round(0)",
"after": "days_reading: 'if(started, (today() - date(started)).days, \"\")'",
"why": "Reads a Duration field instead of rounding the Duration, and guards notes without started (single quotes because the formula holds double quotes)."
},
{
"where": "views[0].groupBy.direction",
"before": "direction: asc",
"after": "direction: ASC",
"why": "groupBy.direction takes ASC or DESC in uppercase."
}
],
"fixed_yaml": "filters:\n and:\n - file.hasTag(\"book\")\nformulas:\n days_reading: 'if(started, (today() - date(started)).days, \"\")'\nviews:\n - type: table\n name: Reading\n order:\n - file.name\n - author\n - formula.days_reading\n groupBy:\n property: status\n direction: ASC\n",
"to_check": [
"That your book notes spell the property started exactly so - no sample notes were given, so the page could not compare names.",
"That status is the property you want the Reading table grouped by."
]
}
verdict describes the original file: broken when the YAML does not parse or
any real finding is an error, fix_first when only warnings are real, ok when nothing
needs to change. Each before is copied exactly from base ("" for an added line).
Worked example: build
A recipe base described in one sentence, with three sample notes pasted into the page. facts.notes summarises those notes (folders, tags, and each property with its note count, types and up to three examples), so the model can use the vault's exact property names:
{
"task": "build",
"request": "All my recipes (tagged recipe) in a table sorted by rating, highest first, with cuisine, minutes and days since I last cooked each one; and a cards view of the quick ones under 30 minutes.",
"facts": "{\"preview_clock\":\"2026-09-27 12:00\",\"base_path\":\"Recipes.base\",\"notes\":{\"files\":3,\"markdown\":3,\"extensions\":{\"md\":3},\"folders\":[\"Recipes\"],\"tags\":[\"quick\",\"recipe\"],\"properties\":[{\"name\":\"cuisine\",\"notes\":3,\"types\":[\"string\"],\"examples\":[\"Middle Eastern\",\"Indian\",\"Thai\"]},{\"name\":\"last_cooked\",\"notes\":2,\"types\":[\"date\"],\"examples\":[\"2026-09-12\",\"2026-08-30\"]},{\"name\":\"minutes\",\"notes\":3,\"types\":[\"number\"],\"examples\":[\"30\",\"45\",\"20\"]},{\"name\":\"rating\",\"notes\":3,\"types\":[\"number\"],\"examples\":[\"5\",\"4\"]},{\"name\":\"tags\",\"notes\":3,\"types\":[\"list\"],\"examples\":[\"recipe\",\"recipe, quick\"]}],\"properties_total\":5}}"
}
A reply in the lane's shape (illustrative; the page re-checked this base_yaml over the same notes: no findings, 3 rows in the table and 1 in Quick):
{
"task": "build",
"headline": "Your recipe notes in a table by rating, highest first, with days since each was last cooked, plus a cards view of recipes under 30 minutes.",
"base_yaml": "filters:\n and:\n - file.hasTag(\"recipe\")\nformulas:\n days_since_cooked: 'if(last_cooked, (today() - date(last_cooked)).days, \"\")'\nproperties:\n formula.days_since_cooked:\n displayName: Days since cooked\nviews:\n - type: table\n name: All recipes\n order:\n - file.name\n - cuisine\n - minutes\n - rating\n - formula.days_since_cooked\n sort:\n - property: rating\n direction: DESC\n - type: cards\n name: Quick\n filters:\n and:\n - \"minutes < 30\"\n order:\n - file.name\n - cuisine\n - minutes\n",
"explanation": [
{
"part": "filters",
"text": "Keeps notes tagged recipe; file.hasTag also matches nested tags such as recipe/dessert."
},
{
"part": "formulas",
"text": "days_since_cooked reads .days from today() minus last_cooked, and is empty on a recipe with no last_cooked (Dal tadka)."
},
{
"part": "properties",
"text": "Labels the formula column Days since cooked."
},
{
"part": "views",
"text": "All recipes is a table sorted by rating DESC; Quick is a cards view that adds the filter minutes < 30."
}
],
"properties_used": [
"cuisine",
"minutes",
"rating",
"last_cooked"
],
"assumptions": [
"Under 30 minutes is read as minutes < 30, so a 30-minute recipe (Shakshuka) is not in Quick; the quick tag is not used."
],
"to_check": [
"Whether Quick should show notes tagged quick instead - swap the view filter for file.hasTag(\"quick\")."
]
}
properties_used lists note properties only (never file.* or formula.*).
Every property the file reads that is not in facts.notes.properties should appear in
assumptions; with facts.notes null, every property name is a guess and is listed there.
Input fields
Every field is a string; facts is JSON text, not an object.
| field | type | required | meaning |
|---|---|---|---|
task | string | yes | "fix" or "build". |
facts | string | yes | What the page's checker established, as JSON text (below). "{}" is accepted. |
base | string | fix; optional for build | The .base file (YAML). Fix: the file to repair. Build: an existing file to extend instead of starting over. The page sends at most 30,000 characters, keeping whole lines from the head and the tail around a YAML comment that says how many middle lines were cut, and records the cut in facts.base_sent. |
symptom | string | no | Fix lane: what you saw in Obsidian (an error in a column, rows missing). At most 2,000 characters in the page. Addressed in headline or a change. |
request | string | build | What the base should show: which notes (tag, folder or property), which columns, how sorted or grouped, any totals or top-N. The page asks for at least 15 characters and sends at most 4,000. |
retry_note | string | no | Only on a reformat retry, when a reply was not one JSON object. |
The facts string
Fix lane (from the page's fixFacts):
{"checker": "Base File Fixer (YAML, Bases schema, expression parse and type checks, preview over pasted notes)",
"status": "broken|fix_first|ok",
"preview_clock": "2026-09-27 12:00",
"base_path": "Reading.base",
"findings": [{"id": "F1", "severity": "error|warning|info", "source": "yaml|schema|expr|data",
"code": "duration-method", "where": "formulas.days_reading", "line": 5,
"text": "...", "quote": "..."}],
"findings_total": 3,
"formula_types": {"days_reading": "number"},
"notes": {...} | null,
"preview": [...] | null,
"base_sent": {"lines": 16, "lines_cut": 0}}
findings holds at most the first 60 (findings_total is the full count); ids run
F1, F2, ... in order, and the reply answers each exactly once, in id order.
where names the key (views[0].groupBy.direction), line is the file line or
null, quote the expression involved. A yaml, schema or
expr error is treated as true unless the file proves otherwise; data findings come from
running the file over your sample notes. formula_types is the type pass's result per formula.
preview_clock is the now()/today() the preview used.
notes (both lanes, null without sample notes):
{"files", "markdown", "extensions", "folders", "tags", "properties": [{"name", "notes", "types", "examples"}], "properties_total"}
— at most 40 folders, 60 tags and 80 properties. preview (fix lane, null without notes)
has one entry per view: {"view", "type", "matched", "shown", "of", "skipped", "columns", "filter_errors", "cell_errors", "first_rows"}.
Build lane (from the page's buildFacts):
{"preview_clock": "2026-09-27 12:00",
"base_path": "Recipes.base",
"notes": {...} | null,
"current_base": {"status": "broken|fix_first|ok", "errors": 0, "warnings": 1}}
current_base is present only when a file was in the page's editor.
You can build facts yourself. If you have not run the browser checker, send "{}"
or a minimal string such as {"findings":[],"notes":null}: the model then works from the file
(or the request) alone, has no findings to answer, and treats every property name it uses in a build as a
guess. The page's checks are what make a fix accountable, so for the full picture paste the file (and a
few notes) into the page; after a run, Download .json saves the exact facts
it sent as facts_sent.
The output
One JSON object, serialised as a string at data.output.output. Parse it and check that task is the lane you sent.
fix:
{"task": "fix", "headline": "", "verdict": "broken|fix_first|ok",
"finding_responses": [{"ref": "F1", "stance": "fixed|not_a_problem|needs_you", "note": ""}],
"changes": [{"where": "formulas.age", "before": "", "after": "", "why": ""}],
"fixed_yaml": "the complete corrected .base file, or \"\"",
"to_check": [""]}
build:
{"task": "build", "headline": "", "base_yaml": "the complete .base file",
"explanation": [{"part": "filters|formulas|properties|summaries|views", "text": ""}],
"properties_used": [""], "assumptions": [""], "to_check": [""]}
stance: fixed (fixed_yaml resolves it), not_a_problem (the
finding does not apply, with the reason), needs_you (it depends on your vault: a property name
only you know, a plugin, your intent). An info finding is fixed if the file changed for it,
otherwise not_a_problem. Every YAML file in a reply is complete, not a diff.
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":"basefile-fixer"} in
its body), so there is no slug header afterwards. Send it as Authorization: Bearer ....
The input object IS the request body. There is no {"input": ...}
wrapper: a wrapped body reaches the model as an object with an input key and no
task, so it cannot route the request. Note that /estimate does not validate
the body at all - a clean estimate does not prove the input is right.
Error codes
| status | code | what to do |
|---|---|---|
| 400 | validation_error | The request was rejected as malformed. Send a JSON object; keep every field a string (facts is a JSON-encoded string, not an object), which is the shape the app itself sends. |
| 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 the app does not sponsor. 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.
In every language the steps below continue the same script: Go and Java put steps 2 to 7 inside
main, in order.
# 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="basefile-fixer"
TOKEN="${SKILLSAFE_TOKEN:-YOUR_TOKEN}" # from https://basefile-fixer.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 = "basefile-fixer"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://basefile-fixer.skillsafe.ai/tokens.html
def call(path, body=None, key=None):
"""Returns the unwrapped `data`, or raises with the API error code.
`key`, when given, is sent as the Idempotency-Key header."""
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")
if key:
req.add_header("Idempotency-Key", key)
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as e:
payload = json.load(e)
if not payload.get("ok"):
err = payload.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
return payload["data"]
// Node 18+, saved as an ES module (client.mjs) so top-level await works.
import { readFileSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "basefile-fixer";
// Paste the token from https://basefile-fixer.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 {}
// Returns the unwrapped `data`, or throws with the API error code.
// `key`, when given, is sent as the Idempotency-Key header.
async function call(path, body, key) {
const headers = { Authorization: `Bearer ${TOKEN}` };
if (body) headers["Content-Type"] = "application/json";
if (key) headers["Idempotency-Key"] = key;
const res = await fetch(`${BASE}/${path}`, {
method: body ? "POST" : "GET",
headers,
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 = "basefile-fixer"
)
var token = os.Getenv("SKILLSAFE_TOKEN") // from https://basefile-fixer.skillsafe.ai/tokens.html
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
// call returns the unwrapped data. A non-empty key is sent as the Idempotency-Key header.
func call(path string, body any, key string) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if body != nil {
method = http.MethodPost
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
rdr = bytes.NewReader(b)
}
req, err := http.NewRequest(method, base+"/"+path, rdr)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if key != "" {
req.Header.Set("Idempotency-Key", key)
}
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
}
// must turns a call result into a map, or stops the program.
func must(raw json.RawMessage, err error) map[string]any {
if err != nil {
panic(err)
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
panic(err)
}
return m
}
func main() {
// steps 2 to 7 go here, in order
}
// Java 17+, with Jackson (com.fasterxml.jackson.core:jackson-databind) for JSON:
// the JDK has no JSON parser of its own.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.Iterator;
import java.util.Map;
public class BaseFileFixer {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "basefile-fixer";
static String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static final ObjectMapper JSON = new ObjectMapper();
// Returns the unwrapped data, or throws with the API error code. key may be null.
static JsonNode call(String path, String jsonBody, String key) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN);
if (key != null) b.header("Idempotency-Key", key);
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());
JsonNode env = JSON.readTree(res.body());
if (!env.path("ok").asBoolean()) {
JsonNode e = env.path("error");
throw new RuntimeException(e.path("code").asText() + ": " + e.path("message").asText());
}
return env.path("data");
}
public static void main(String[] args) throws Exception {
// steps 2 to 7 go here, in order
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "basefile-fixer"
$token = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://basefile-fixer.skillsafe.ai/tokens.html
# Returns the unwrapped data, or raises with the API error code.
# key, when given, is sent as the Idempotency-Key header.
def call(path, body = nil, key = nil)
uri = URI("#{BASE}/#{path}")
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{$token}"
req["Idempotency-Key"] = key if key
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
// PHP 8+. The later steps continue this file.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "basefile-fixer";
$GLOBALS["token"] = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"; // from /tokens.html
// Returns the unwrapped data, or throws with the API error code.
// $key, when given, is sent as the Idempotency-Key header.
function call(string $path, ?array $body = null, ?string $key = null) {
$ch = curl_init(BASE . "/" . $path);
$headers = ["Authorization: Bearer " . $GLOBALS["token"]];
if ($key !== null) {
$headers[] = "Idempotency-Key: " . $key;
}
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"];
}
// Program.cs (.NET 6+, top-level statements). The later steps continue this file.
using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "basefile-fixer";
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
var http = new HttpClient();
// Returns the unwrapped data, or throws with the API error code.
// json is the request body as JSON text; key, when given, is the Idempotency-Key.
async Task<JsonElement> Call(string path, string? json = null, string? key = null)
{
var req = new HttpRequestMessage(json == null ? HttpMethod.Get : HttpMethod.Post, $"{Base}/{path}");
req.Headers.Add("Authorization", $"Bearer {token}");
if (key != null) req.Headers.Add("Idempotency-Key", key);
if (json != null) req.Content = new StringContent(json, Encoding.UTF8, "application/json");
using var res = await http.SendAsync(req);
using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
var payload = doc.RootElement;
if (!payload.GetProperty("ok").GetBoolean())
{
var e = payload.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return payload.GetProperty("data").Clone();
}
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":"basefile-fixer"}, can call /me and
/estimate; both lanes are metered, so /run and /run-stream need
a personal token unless /estimate reports sponsor_enabled: true.
# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
# https://basefile-fixer.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 "$BASE/guest" \
-H "Content-Type: application/json" -d '{"slug":"basefile-fixer"}'
# {"ok":true,"data":{"token":"...","subject_type":"guest"}}
# Open https://basefile-fixer.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.
if TOKEN == "YOUR_TOKEN":
req = urllib.request.Request(f"{BASE}/guest", data=json.dumps({"slug": SLUG}).encode(), method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
// Open https://basefile-fixer.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.
if (TOKEN === "YOUR_TOKEN") {
const guestRes = await fetch(`${BASE}/guest`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: SLUG }),
});
TOKEN = (await guestRes.json()).data.token;
}
// Open https://basefile-fixer.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.
if token == "" {
gres, err := http.Post(base+"/guest", "application/json",
strings.NewReader(`{"slug":"`+slug+`"}`))
if err != nil {
panic(err)
}
var guest struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
_ = json.NewDecoder(gres.Body).Decode(&guest)
gres.Body.Close()
token = guest.Data.Token
}
// Open https://basefile-fixer.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.
if (TOKEN.equals("YOUR_TOKEN")) {
HttpRequest guestReq = HttpRequest.newBuilder(URI.create(BASE + "/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"" + SLUG + "\"}"))
.build();
String guest = HTTP.send(guestReq, HttpResponse.BodyHandlers.ofString()).body();
TOKEN = JSON.readTree(guest).path("data").path("token").asText();
}
# Open https://basefile-fixer.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.
if $token == "YOUR_TOKEN"
uri = URI("#{BASE}/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = { slug: SLUG }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
$token = JSON.parse(res.body)["data"]["token"]
end
// Open https://basefile-fixer.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.
if ($GLOBALS["token"] === "YOUR_TOKEN") {
$ch = curl_init(BASE . "/guest");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["slug" => SLUG]));
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);
$GLOBALS["token"] = $guest["data"]["token"];
}
// Open https://basefile-fixer.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.
if (token == "YOUR_TOKEN")
{
var guestReq = new HttpRequestMessage(HttpMethod.Post, $"{Base}/guest");
guestReq.Content = new StringContent("{\"slug\":\"" + Slug + "\"}", Encoding.UTF8, "application/json");
using var guestRes = await http.SendAsync(guestReq);
using var guestDoc = JsonDocument.Parse(await guestRes.Content.ReadAsStringAsync());
token = guestDoc.RootElement.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);
me := must(call("me", nil, ""))
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = call("me", null, null);
System.out.println(me.path("subject_type").asText() + " " + me.path("credits"));
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
$me = call("me");
echo $me["subject_type"], " ", $me["credits"] ?? "-", PHP_EOL;
var me = await Call("me");
var credits = me.TryGetProperty("credits", out var c) ? c.ToString() : "-";
Console.WriteLine($"{me.GetProperty("subject_type").GetString()} {credits}");
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
fix or build, a facts that is a JSON string parsing to an object,
and a non-empty base (fix) or request (build). Save one of the worked
examples above as body.json to try it.
# 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 ("fix", "build"), "task must be fix or build"
assert all(isinstance(v, str) for v in b.values()), "every field is a string"
assert isinstance(json.loads(b["facts"]), dict), "facts is a JSON string of an object"
need = "base" if b["task"] == "fix" else "request"
assert b.get(need, "").strip(), b["task"] + " needs " + need
'
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.
with open("body.json") as fh:
INPUT = json.load(fh) # the input object itself, no wrapper
assert isinstance(INPUT, dict) and INPUT.get("task") in ("fix", "build"), "task must be fix or build"
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"
need = "base" if INPUT["task"] == "fix" else "request"
assert INPUT.get(need, "").strip(), f"{INPUT['task']} needs {need}"
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" || !["fix", "build"].includes(INPUT.task)) throw new Error("task must be fix or build");
if (!Object.values(INPUT).every((v) => typeof v === "string")) throw new Error("every field is a string");
const facts = JSON.parse(INPUT.facts);
if (!facts || typeof facts !== "object" || Array.isArray(facts)) throw new Error("facts is a JSON string of an object");
const need = INPUT.task === "fix" ? "base" : "request";
if (!(INPUT[need] || "").trim()) throw new Error(`${INPUT.task} needs ${need}`);
const est = await call("estimate", INPUT);
console.log(est.model_alias, "reserves", est.hold_credits, "credits (not the price)");
raw, err := os.ReadFile("body.json")
if err != nil {
panic(err)
}
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())
}
lane := input["task"]
if lane != "fix" && lane != "build" {
panic("task must be fix or build")
}
var facts map[string]any
if err := json.Unmarshal([]byte(input["facts"]), &facts); err != nil || facts == nil {
panic("facts must be a JSON string of an object")
}
need := "request"
if lane == "fix" {
need = "base"
}
if strings.TrimSpace(input[need]) == "" {
panic(lane + " needs " + need)
}
est := must(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, sent as is
JsonNode in = JSON.readTree(input);
String lane = in.path("task").asText();
if (!in.isObject() || !(lane.equals("fix") || lane.equals("build")))
throw new IllegalStateException("task must be fix or build");
for (Iterator<Map.Entry<String, JsonNode>> it = in.fields(); it.hasNext(); )
if (!it.next().getValue().isTextual()) throw new IllegalStateException("every field is a string");
if (!JSON.readTree(in.path("facts").asText()).isObject())
throw new IllegalStateException("facts is a JSON string of an object");
String need = lane.equals("fix") ? "base" : "request";
if (in.path(need).asText().isBlank()) throw new IllegalStateException(lane + " needs " + need);
JsonNode est = call("estimate", input, null);
System.out.println(est.path("model_alias").asText() + " reserves " + est.path("hold_credits")
+ " credits (not the price)");
INPUT = JSON.parse(File.read("body.json")) # no {"input": ...} wrapper
raise "task must be fix or build" unless INPUT.is_a?(Hash) && %w[fix build].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)
need = INPUT["task"] == "fix" ? "base" : "request"
raise "#{INPUT['task']} needs #{need}" if INPUT[need].to_s.strip.empty?
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"] ?? "", ["fix", "build"], true)) { throw new Exception("task must be fix or build"); }
foreach ($input as $v) { if (!is_string($v)) { throw new Exception("every field is a string"); } }
if (!is_object(json_decode($input["facts"] ?? ""))) { throw new Exception("facts is a JSON string of an object"); }
$need = $input["task"] === "fix" ? "base" : "request";
if (trim($input[$need] ?? "") === "") { throw new Exception($input["task"] . " needs " . $need); }
$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, sent as is
using var inputDoc = JsonDocument.Parse(input);
var root = inputDoc.RootElement;
if (root.ValueKind != JsonValueKind.Object) throw new Exception("body.json must be an object");
foreach (var p in root.EnumerateObject())
if (p.Value.ValueKind != JsonValueKind.String) throw new Exception("every field is a string");
var lane = root.TryGetProperty("task", out var t) ? t.GetString() : null;
if (lane != "fix" && lane != "build") throw new Exception("task must be fix or build");
using (var factsDoc = JsonDocument.Parse(root.GetProperty("facts").GetString()!))
if (factsDoc.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("facts is a JSON string of an object");
var need = lane == "fix" ? "base" : "request";
if (!root.TryGetProperty(need, out var nv) || string.IsNullOrWhiteSpace(nv.GetString()))
throw new Exception($"{lane} needs {need}");
var est = await Call("estimate", input);
Console.WriteLine($"{est.GetProperty("model_alias")} reserves {est.GetProperty("hold_credits")} credits (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: parse it (step 7). Send an
Idempotency-Key built from the lane, a hash of the input and the attempt number,
basefile-fixer:<lane>:<hash>:a<attempt>, so a retried request returns the
same job instead of billing a second run. Use one key per distinct input: an edited base, facts,
symptom or request is 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"])') # fix or build
KEY="basefile-fixer:$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\":\"fix\",\"headline\":\"...\",\"verdict\":\"broken\", ...}"},
# "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"basefile-fixer:{INPUT['task']}:{digest}:a1"
job_id = call("run", INPUT, key)["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"))
const digest = createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16);
const key = `basefile-fixer:${INPUT.task}:${digest}:a1`;
const jobId = (await call("run", INPUT, key)).job_id;
let job;
do {
if (job) await new Promise((r) => setTimeout(r, 2000));
job = await call(`jobs/${jobId}`);
} while (job.status !== "succeeded" && job.status !== "failed");
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const text = job.output.output; // the reply, as a string
console.log("charged", job.charged_credits, "truncated", job.truncated);
body, _ := json.Marshal(input) // Go sorts map keys, so this digest is stable
sum := sha256.Sum256(body)
key := fmt.Sprintf("basefile-fixer:%s:%x:a1", lane, sum[:8])
jobID := must(call("run", input, key))["job_id"].(string)
var job map[string]any
for {
job = must(call("jobs/"+jobID, nil, ""))
if job["status"] == "succeeded" || job["status"] == "failed" {
break
}
time.Sleep(2 * time.Second)
}
if job["status"] == "failed" {
panic(fmt.Sprint(job["error"]))
}
text := job["output"].(map[string]any)["output"].(string) // the reply, as a string
fmt.Println("charged", job["charged_credits"], "truncated", job["truncated"])
String hash = HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256").digest(input.getBytes(StandardCharsets.UTF_8))).substring(0, 16);
String key = "basefile-fixer:" + lane + ":" + hash + ":a1";
String jobId = call("run", input, key).path("job_id").asText();
JsonNode job;
while (true) {
job = call("jobs/" + jobId, null, null);
String status = job.path("status").asText();
if (status.equals("succeeded")) break;
if (status.equals("failed")) throw new RuntimeException(job.toString());
Thread.sleep(2000);
}
String text = job.path("output").path("output").asText(); // the reply, as a string
System.out.println("charged " + job.path("charged_credits") + " truncated " + job.path("truncated"));
require "digest"
key = "basefile-fixer:#{INPUT['task']}:#{Digest::SHA256.hexdigest(INPUT.to_json)[0, 16]}:a1"
job_id = call("run", INPUT, key)["job_id"]
job = nil
loop do
job = call("jobs/#{job_id}")
break if %w[succeeded failed].include?(job["status"])
sleep 2
end
raise job.inspect if job["status"] == "failed"
text = job["output"]["output"] # the reply, as a string
puts "charged #{job['charged_credits']} truncated #{job['truncated']}"
$key = "basefile-fixer:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$jobId = call("run", $input, $key)["job_id"];
while (true) {
$job = call("jobs/" . $jobId);
if (in_array($job["status"], ["succeeded", "failed"], true)) { break; }
sleep(2);
}
if ($job["status"] === "failed") { throw new RuntimeException(json_encode($job)); }
$text = $job["output"]["output"]; // the reply, as a string
echo "charged ", $job["charged_credits"] ?? "?", PHP_EOL;
var hash = BitConverter.ToString(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(input)))
.Replace("-", "").ToLowerInvariant()[..16];
var key = $"basefile-fixer:{lane}:{hash}:a1";
var started = await Call("run", input, key);
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await 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 text = job.GetProperty("output").GetProperty("output").GetString()!; // the reply, as a string
Console.WriteLine($"charged {job.GetProperty("charged_credits")}");
6. Or stream it
POST /run-stream takes the same body and headers as /run (use it instead of
step 5, not after it with the same key; build the key as step 5 does) 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\":\"fix\",\"headline\":\"formulas.days_reading"}
# 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)
deltas, done, event = "", {}, None
with urllib.request.urlopen(req) as stream:
for line in stream:
line = line.decode().rstrip("\r\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
deltas += json.loads(line[6:]).get("text", "")
elif line.startswith("data: ") and event == "done":
done = json.loads(line[6:])
streamed = (done.get("output") or {}).get("output") or deltas # browsers may get only ticks + done
print(done.get("status"), done.get("charged_credits"), done.get("truncated"))
const sres = 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 = sres.body.getReader();
const dec = new TextDecoder();
let buf = "", deltas = "", event = null, doneEvent = null;
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i).replace(/\r$/, "");
buf = buf.slice(i + 1);
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ") && event === "delta") deltas += JSON.parse(line.slice(6)).text || "";
else if (line.startsWith("data: ") && event === "done") doneEvent = JSON.parse(line.slice(6));
}
}
const streamed = doneEvent?.output?.output || deltas; // browsers may get only ticks + done
console.log(doneEvent?.status, streamed.length);
sreq, _ := http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(body))
sreq.Header.Set("Authorization", "Bearer "+token)
sreq.Header.Set("Content-Type", "application/json")
sreq.Header.Set("Idempotency-Key", key)
sreq.Header.Set("Accept", "text/event-stream")
sres, err := http.DefaultClient.Do(sreq)
if err != nil {
panic(err)
}
defer sres.Body.Close()
var deltas strings.Builder
event, streamed := "", ""
sc := bufio.NewScanner(sres.Body)
sc.Buffer(make([]byte, 1<<20), 4<<20)
for sc.Scan() {
line := strings.TrimRight(sc.Text(), "\r")
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)
deltas.WriteString(d.Text)
case strings.HasPrefix(line, "data: ") && event == "done":
var d struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
_ = json.Unmarshal([]byte(line[6:]), &d)
streamed = d.Output.Output
fmt.Println("done:", d.Status)
}
}
if streamed == "" {
streamed = deltas.String() // browsers may get only ticks + done
}
fmt.Println(len(streamed), "characters streamed")
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();
StringBuilder deltas = new StringBuilder();
String[] event = {""};
String[] fromDone = {""};
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.startsWith("event: ")) {
event[0] = line.substring(7);
} else if (line.startsWith("data: ")) {
try {
JsonNode d = JSON.readTree(line.substring(6));
if (event[0].equals("delta")) deltas.append(d.path("text").asText());
if (event[0].equals("done")) fromDone[0] = d.path("output").path("output").asText();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
String streamed = fromDone[0].isEmpty() ? deltas.toString() : fromDone[0]; // browsers may get only ticks + done
System.out.println(streamed.length() + " characters streamed");
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
buf, deltas, event, streamed = +"", +"", nil, nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |h|
h.request(req) do |res|
res.read_body do |chunk|
buf << chunk
while (i = buf.index("\n"))
line = buf.slice!(0, i + 1).chomp
if line.start_with?("event: ")
event = line[7..-1]
elsif line.start_with?("data: ") && event == "delta"
deltas << JSON.parse(line[6..-1])["text"].to_s
elsif line.start_with?("data: ") && event == "done"
streamed = (JSON.parse(line[6..-1])["output"] || {})["output"]
end
end
end
end
end
streamed = deltas if streamed.to_s.empty? # browsers may get only ticks + done
puts "#{streamed.length} characters streamed"
$buf = ""; $deltas = ""; $event = null; $streamed = "";
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $GLOBALS["token"], "Content-Type: application/json",
"Idempotency-Key: " . $key, "Accept: text/event-stream"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$buf, &$deltas, &$event, &$streamed) {
$buf .= $chunk;
while (($i = strpos($buf, "\n")) !== false) {
$line = rtrim(substr($buf, 0, $i), "\r");
$buf = substr($buf, $i + 1);
if (str_starts_with($line, "event: ")) {
$event = substr($line, 7);
} elseif (str_starts_with($line, "data: ") && $event === "delta") {
$deltas .= json_decode(substr($line, 6), true)["text"] ?? "";
} elseif (str_starts_with($line, "data: ") && $event === "done") {
$streamed = json_decode(substr($line, 6), true)["output"]["output"] ?? "";
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
if ($streamed === "") { $streamed = $deltas; } // browsers may get only ticks + done
echo strlen($streamed), " characters streamed\n";
var sreq = new HttpRequestMessage(HttpMethod.Post, $"{Base}/run-stream");
sreq.Headers.Add("Authorization", $"Bearer {token}");
sreq.Headers.Add("Idempotency-Key", key);
sreq.Headers.Add("Accept", "text/event-stream");
sreq.Content = new StringContent(input, Encoding.UTF8, "application/json");
using var sres = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var sr = new StreamReader(await sres.Content.ReadAsStreamAsync());
var deltas = new StringBuilder();
string? ev = null, line, fromDone = null;
while ((line = await sr.ReadLineAsync()) != null)
{
if (line.StartsWith("event: ")) ev = line[7..];
else if (line.StartsWith("data: ") && ev == "delta")
{
using var d = JsonDocument.Parse(line[6..]);
if (d.RootElement.TryGetProperty("text", out var tx)) deltas.Append(tx.GetString());
}
else if (line.StartsWith("data: ") && ev == "done")
{
using var d = JsonDocument.Parse(line[6..]);
if (d.RootElement.TryGetProperty("output", out var o) && o.ValueKind == JsonValueKind.Object
&& o.TryGetProperty("output", out var oo)) fromDone = oo.GetString();
}
}
var streamed = string.IsNullOrEmpty(fromDone) ? deltas.ToString() : fromDone; // browsers may get only ticks + done
Console.WriteLine($"{streamed.Length} characters streamed");
7. Parse the reply, and re-check the file
The reply is one JSON object serialised as a string (text from step 5, or
streamed from step 6). Parse it and check that task is the lane you asked for.
Then check the file it returns (fixed_yaml for a fix, base_yaml for a build)
before you use it: paste it into the page, with your sample notes, which runs the same YAML, schema and
expression checks and previews every view. An empty fixed_yaml means nothing needed to change.
# 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["headline"])
if r["task"] == "fix":
print("verdict:", r["verdict"])
for f in r["finding_responses"]: print(f["ref"], f["stance"], f["note"])
for c in r["changes"]: print("-", c["where"], ":", c["before"], "->", c["after"])
if r["fixed_yaml"]: open("fixed.base", "w").write(r["fixed_yaml"])
else:
for e in r["explanation"]: print(e["part"], "-", e["text"])
print("assumptions:", r["assumptions"])
open("new.base", "w").write(r["base_yaml"])
for x in r["to_check"]: print("check:", x)
'
# Paste fixed.base or new.base into https://basefile-fixer.skillsafe.ai/ to re-check and preview it.
reply = json.loads(text) # or `streamed` from step 6
assert reply["task"] == INPUT["task"], "the model answered as the other lane"
print(reply["headline"])
if reply["task"] == "fix":
print("verdict:", reply["verdict"])
for r in reply["finding_responses"]:
print(r["ref"], r["stance"], r["note"])
for c in reply["changes"]:
print("-", c["where"], ":", c["before"], "->", c["after"])
if reply["fixed_yaml"]:
with open("fixed.base", "w") as fh:
fh.write(reply["fixed_yaml"])
else:
for e in reply["explanation"]:
print(e["part"], "-", e["text"])
print("assumptions:", reply["assumptions"])
with open("new.base", "w") as fh:
fh.write(reply["base_yaml"])
for x in reply["to_check"]:
print("check:", x)
const reply = JSON.parse(text); // or `streamed` from step 6
if (reply.task !== INPUT.task) throw new Error("the model answered as the other lane");
console.log(reply.headline);
if (reply.task === "fix") {
console.log("verdict:", reply.verdict);
for (const r of reply.finding_responses) console.log(r.ref, r.stance, r.note);
for (const c of reply.changes) console.log("-", c.where, ":", c.before, "->", c.after);
if (reply.fixed_yaml) writeFileSync("fixed.base", reply.fixed_yaml);
} else {
for (const e of reply.explanation) console.log(e.part, "-", e.text);
console.log("assumptions:", reply.assumptions);
writeFileSync("new.base", reply.base_yaml);
}
for (const x of reply.to_check) console.log("check:", x);
var reply map[string]any
if err := json.Unmarshal([]byte(text), &reply); err != nil { // or streamed from step 6
panic("the reply is not one JSON object: " + err.Error())
}
if reply["task"] != lane {
panic("the model answered as the other lane")
}
fmt.Println(reply["headline"])
if lane == "fix" {
fmt.Println("verdict:", reply["verdict"])
for _, x := range reply["finding_responses"].([]any) {
r := x.(map[string]any)
fmt.Println(r["ref"], r["stance"], r["note"])
}
for _, x := range reply["changes"].([]any) {
c := x.(map[string]any)
fmt.Println("-", c["where"], ":", c["before"], "->", c["after"])
}
if y, _ := reply["fixed_yaml"].(string); y != "" {
if err := os.WriteFile("fixed.base", []byte(y), 0o644); err != nil {
panic(err)
}
}
} else {
y, _ := reply["base_yaml"].(string)
if err := os.WriteFile("new.base", []byte(y), 0o644); err != nil {
panic(err)
}
fmt.Println("assumptions:", reply["assumptions"])
}
JsonNode reply = JSON.readTree(text); // or streamed from step 6
if (!reply.path("task").asText().equals(lane))
throw new IllegalStateException("the model answered as the other lane");
System.out.println(reply.path("headline").asText());
if (lane.equals("fix")) {
System.out.println("verdict: " + reply.path("verdict").asText());
for (JsonNode r : reply.path("finding_responses"))
System.out.println(r.path("ref").asText() + " " + r.path("stance").asText() + " " + r.path("note").asText());
for (JsonNode c : reply.path("changes"))
System.out.println("- " + c.path("where").asText() + " : " + c.path("before").asText()
+ " -> " + c.path("after").asText());
String fixedYaml = reply.path("fixed_yaml").asText();
if (!fixedYaml.isEmpty()) Files.writeString(Path.of("fixed.base"), fixedYaml);
} else {
Files.writeString(Path.of("new.base"), reply.path("base_yaml").asText());
for (JsonNode a : reply.path("assumptions")) System.out.println("assumption: " + a.asText());
}
reply = JSON.parse(text) # or streamed from step 6
raise "the model answered as the other lane" unless reply["task"] == INPUT["task"]
puts reply["headline"]
if reply["task"] == "fix"
puts "verdict: #{reply['verdict']}"
reply["finding_responses"].each { |r| puts "#{r['ref']} #{r['stance']} #{r['note']}" }
reply["changes"].each { |c| puts "- #{c['where']} : #{c['before']} -> #{c['after']}" }
File.write("fixed.base", reply["fixed_yaml"]) unless reply["fixed_yaml"].to_s.empty?
else
reply["explanation"].each { |e| puts "#{e['part']} - #{e['text']}" }
puts "assumptions: #{reply['assumptions'].join('; ')}"
File.write("new.base", reply["base_yaml"])
end
$reply = json_decode($text, true); // or $streamed from step 6
if (!is_array($reply) || ($reply["task"] ?? "") !== $input["task"]) { throw new Exception("the model answered as the other lane"); }
echo $reply["headline"], "\n";
if ($reply["task"] === "fix") {
echo "verdict: ", $reply["verdict"], "\n";
foreach ($reply["finding_responses"] as $r) { echo $r["ref"], " ", $r["stance"], " ", $r["note"], "\n"; }
foreach ($reply["changes"] as $c) { echo "- ", $c["where"], " : ", $c["before"], " -> ", $c["after"], "\n"; }
if (($reply["fixed_yaml"] ?? "") !== "") { file_put_contents("fixed.base", $reply["fixed_yaml"]); }
} else {
foreach ($reply["explanation"] as $e) { echo $e["part"], " - ", $e["text"], "\n"; }
echo "assumptions: ", implode("; ", $reply["assumptions"]), "\n";
file_put_contents("new.base", $reply["base_yaml"]);
}
using var replyDoc = JsonDocument.Parse(text); // or streamed from step 6
var reply = replyDoc.RootElement;
if (reply.GetProperty("task").GetString() != lane) throw new Exception("the model answered as the other lane");
Console.WriteLine(reply.GetProperty("headline").GetString());
if (lane == "fix")
{
Console.WriteLine($"verdict: {reply.GetProperty("verdict")}");
foreach (var r in reply.GetProperty("finding_responses").EnumerateArray())
Console.WriteLine($"{r.GetProperty("ref")} {r.GetProperty("stance")} {r.GetProperty("note")}");
foreach (var c in reply.GetProperty("changes").EnumerateArray())
Console.WriteLine($"- {c.GetProperty("where")} : {c.GetProperty("before")} -> {c.GetProperty("after")}");
var fixedYaml = reply.GetProperty("fixed_yaml").GetString();
if (!string.IsNullOrEmpty(fixedYaml)) File.WriteAllText("fixed.base", fixedYaml);
}
else
{
File.WriteAllText("new.base", reply.GetProperty("base_yaml").GetString());
foreach (var a in reply.GetProperty("assumptions").EnumerateArray())
Console.WriteLine($"assumption: {a.GetString()}");
}
Costs
- The checks are free and run in the page: the YAML reader, the Bases schema checks, the expression parse and type pass, and the preview over your notes, plus every export. 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. - Who pays: the caller's own balance, unless
/estimatereturnssponsor_enabled: true. - Runs need a signed-in user token unless the app sponsors them. A guest token can call
/meand/estimate; sign in for a personal token on the token page. - A reformat retry (with
retry_note) is a new attempt with its own key (:a2) 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. - Fix: every finding id in
facts.findingsis answered exactly once infinding_responses, in id order, and no other id is;stanceisfixed,not_a_problemorneeds_you; ayaml,schemaorexprerror is not callednot_a_problemwithout a reason the file proves. - Fix:
verdictis notokwhilefacts.findingsholds a real error; everychanges[].beforeappears in thebaseyou sent (or is""); a finding markedfixedis gone whenfixed_yamlis re-checked. - Build:
base_yamlparses, defines at least one view, and every view has anameand anorder; every name inproperties_usedis infacts.notes.propertiesor listed inassumptions. - Every YAML file in a reply is complete (not a diff) and uses only the top-level keys
filters,formulas,properties,summariesandviews.