Drive FastAPI Clinic from your own code
Everything the web page does is available over HTTP: paste a FastAPI project in, get the same
structured production-readiness review back. The natural use is a CI job that re-reviews whenever
main.py or the routers change, or a script that runs the same twelve-check pass across
every service in a fleet and fails the build when one of them drifts into not-ready.
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": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
Send your app slug as X-App-Slug: fastapi-clinic and your token as
Authorization: Bearer … on every call.
Error codes
| code | status | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Get a new one from the token page. |
payment_required | 402 | The balance is below min_credits. Call /estimate first and top up. |
forbidden | 403 | The token is valid but not for this app, or a guest token tried a metered run. Check the X-App-Slug header. |
not_found | 404 | Unknown job id, unknown collection, or the app slug does not exist. |
conflict | 409 | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
validation_error | 422 | The input object is missing a required field — files is the usual one — or a field is the wrong type. |
rate_limited | 429 | Too many requests. Back off and retry; do not tight-loop. |
internal | 5xx | A server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice. |
1. 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. Nothing on that page needs a developer tool — it reads the same storage the app itself uses and prints the token for you.
A guest token can call /me and /estimate. Running a
review is metered, so it needs a personal token from signing in.
# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
# https://fastapi-clinic.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; running a review needs a personal token from signing in.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" -H "X-App-Slug: fastapi-clinic"
# {"ok":true,"data":{"token":"sk_guest_...","subject_type":"guest"}}
# Open https://fastapi-clinic.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot run a metered review.
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest", data=b"{}", method="POST")
req.add_header("X-App-Slug", "fastapi-clinic")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
// Open https://fastapi-clinic.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a metered review.
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "X-App-Slug": "fastapi-clinic", "Content-Type": "application/json" },
body: "{}",
});
const TOKEN = (await res.json()).data.token;
// Open https://fastapi-clinic.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a metered review.
guestReq, _ := http.NewRequest(http.MethodPost,
"https://api.skillsafe.ai/v1/app-api/guest", bytes.NewReader([]byte("{}")))
guestReq.Header.Set("X-App-Slug", "fastapi-clinic")
guestReq.Header.Set("Content-Type", "application/json")
guestRes, err := http.DefaultClient.Do(guestReq)
if err != nil {
panic(err)
}
defer guestRes.Body.Close()
var guest struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
_ = json.NewDecoder(guestRes.Body).Decode(&guest)
fmt.Println(guest.Data.Token)
// Open https://fastapi-clinic.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a metered review.
var http = HttpClient.newHttpClient();
var guestReq = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("X-App-Slug", "fastapi-clinic")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{}"))
.build();
HttpResponse<String> guest = http.send(guestReq, HttpResponse.BodyHandlers.ofString());
System.out.println(guest.body()); // {"ok":true,"data":{"token":"sk_guest_..."}}
# Open https://fastapi-clinic.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot run a metered review.
require "json"
require "net/http"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
req["X-App-Slug"] = "fastapi-clinic"
req["Content-Type"] = "application/json"
req.body = "{}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
TOKEN = JSON.parse(res.body)["data"]["token"]
<?php
// Open https://fastapi-clinic.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a metered review.
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, "{}");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-App-Slug: fastapi-clinic",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$guest = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $guest["data"]["token"];
// Open https://fastapi-clinic.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot run a metered review.
using var http = new HttpClient();
var guestReq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/guest");
guestReq.Headers.Add("X-App-Slug", "fastapi-clinic");
guestReq.Content = new StringContent("{}", Encoding.UTF8, "application/json");
var guestRes = await http.SendAsync(guestReq);
var guest = await guestRes.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(guest.GetProperty("data").GetProperty("token").GetString());
2. A tiny client
One helper that adds the headers, unwraps data and raises on error.
# 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="fastapi-clinic"
TOKEN="$SKILLSAFE_TOKEN" # from https://fastapi-clinic.skillsafe.ai/tokens.html
call() { # call <path> [json-body]
if [ -n "$2" ]; then
curl -sS -X POST "$BASE/$1" \
-H "Authorization: Bearer $TOKEN" \
-H "X-App-Slug: $SLUG" \
-H "Content-Type: application/json" \
-d "$2"
else
curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG"
fi
}
import json, os, urllib.error, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "fastapi-clinic"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://fastapi-clinic.skillsafe.ai/tokens.html
def call(path, body=None):
"""Returns the unwrapped `data`, or raises with the API error code."""
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(f"{BASE}/{path}", data=data, method="POST" if body is not None else "GET")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("X-App-Slug", SLUG)
if body is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as e:
payload = json.load(e)
if not payload.get("ok"):
err = payload.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "fastapi-clinic";
const TOKEN = "YOUR_TOKEN"; // from https://fastapi-clinic.skillsafe.ai/tokens.html
async function call(path, body) {
const res = await fetch(`${BASE}/${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
"X-App-Slug": SLUG,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const payload = await res.json();
if (!payload.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
return payload.data;
}
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "fastapi-clinic"
)
var token = os.Getenv("SKILLSAFE_TOKEN") // from https://fastapi-clinic.skillsafe.ai/tokens.html
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if body != nil {
method = http.MethodPost
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+"/"+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", slug)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class Clinic {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "fastapi-clinic";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN)
.header("X-App-Slug", SLUG);
if (jsonBody != null) {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
b.GET();
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is always {"ok":true,"data":...} or {"ok":false,"error":...}.
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "fastapi-clinic"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://fastapi-clinic.skillsafe.ai/tokens.html
def call(path, body = nil)
uri = URI("#{BASE}/#{path}")
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "fastapi-clinic";
define("TOKEN", getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"); // from /tokens.html
function call(string $path, ?array $body = null) {
$ch = curl_init(BASE . "/" . $path);
$headers = ["Authorization: Bearer " . TOKEN, "X-App-Slug: " . SLUG];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
using System.Net.Http.Json;
using System.Text.Json;
static class Clinic
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "fastapi-clinic";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(string path, object? body = null)
{
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, $"{Base}/{path}");
req.Headers.Add("Authorization", $"Bearer {Token}");
req.Headers.Add("X-App-Slug", Slug);
if (body is not null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var payload = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!payload.GetProperty("ok").GetBoolean())
{
var e = payload.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return payload.GetProperty("data");
}
}
3. Check the session and the balance
GET /me tells you whether the token is a guest or a person, and what the balance is.
Compare it against min_credits from the next step before you run, so a shortfall
surfaces as your own clear message rather than a 402.
call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
print(me["subject_type"], me.get("credits"))
const me = await call("me");
console.log(me.subject_type, me.credits);
raw, err := call("me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("me", null));
// {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var me = await Clinic.Call("me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
4. Price the run — free
The input object is exactly what the app's own form submits:
| field | type | meaning |
|---|---|---|
files | string, required | The pasted project source — main.py, routers, Pydantic models, dependencies, database session code, middleware, settings, requirements.txt or pyproject.toml. Put a # file: app/main.py marker line above each one so they can be told apart. This is the review's only evidence. A file whose middle has been removed should say so with a # [... clipped ...] comment. |
target | string | What the code is being reviewed against: production, staging, internal or unknown. It changes priority and framing, not what counts as a finding — a blocking call inside async def on internal is still a finding. unknown is reviewed against production, and the review says so in assumptions. |
focus | string | general, async, validation, auth, database or performance. Emphasis, not exclusivity: a high-severity finding from another area is never suppressed. |
context | string, optional | Free-form notes: expected traffic, who calls the service, what it stores, what sits behind a gateway, the deadline. |
prescan_facts | object | {resources: [{id,label}], flags: [{id,label}]} — what the app's free in-browser static analyzer established: routes, dependencies, Pydantic models, middleware and settings in resources; checks that fired in flags. Every flags id must come back in coverage_check, which is how you hold the model to the facts. |
retry_note | string, optional | Send only on a retry, when a previous reply failed to parse or came back truncated. The instruction is obeyed exactly. |
/estimate creates no job and charges nothing. It returns the model
binding — model, model_alias, markup_bps — and the
reservation: hold_credits is what gets held, min_credits is the balance
you must clear to start, and sponsor_enabled says whether the app is covering the run.
The hold is a reservation, not the price. It prices the full output cap, so the
charged_credits you see after settlement is usually far lower — often a small fraction
of the hold. Budget against hold_credits, report against charged_credits.
INPUT='{"files": "# file: app/main.py\nimport requests\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get(\"/orders/{order_id}\")\nasync def get_order(order_id):\n r = requests.get(f\"https://billing.internal/orders/{order_id}\")\n return r.json()\n\n# file: requirements.txt\nfastapi==0.95.2\npydantic==1.10.13", "target": "production", "focus": "async", "context": "Public order API behind an ALB, 300 rps at peak, stores customer addresses, launch in two weeks.", "prescan_facts": {"resources": [{"id": "routes:count", "label": "14 routes declared, 9 async"}, {"id": "dep:fastapi", "label": "fastapi==0.95.2"}], "flags": [{"id": "async:blocking-call", "label": "requests.get inside an async def handler"}, {"id": "response-model:missing", "label": "7 routes declared without response_model"}]}}'
call estimate "$INPUT"
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":1920,"min_credits":290,"sponsor_enabled":false}}
#
# estimate is FREE. It creates no job and charges nothing. hold_credits is what
# gets RESERVED; charged_credits after settlement is normally much lower.
INPUT = {
"files": "# file: app/main.py\nimport requests\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get(\"/orders/{order_id}\")\nasync def get_order(order_id):\n r = requests.get(f\"https://billing.internal/orders/{order_id}\")\n return r.json()\n\n# file: requirements.txt\nfastapi==0.95.2\npydantic==1.10.13",
"target": "production",
"focus": "async",
"context": "Public order API behind an ALB, 300 rps at peak, stores customer addresses, launch in two weeks.",
"prescan_facts": {
"resources": [
{"id": "routes:count", "label": "14 routes declared, 9 async"},
{"id": "dep:fastapi", "label": "fastapi==0.95.2"}
],
"flags": [
{"id": "async:blocking-call", "label": "requests.get inside an async def handler"},
{"id": "response-model:missing", "label": "7 routes declared without response_model"}
]
}
}
est = call("estimate", INPUT)
print(est["model"], est["model_alias"], est["markup_bps"])
print(est["hold_credits"], est["min_credits"], est["sponsor_enabled"])
# estimate is free: no job is created and nothing is charged. The hold is a
# reservation against the full output cap, not the price of the run.
const INPUT = {
files:
'# file: app/main.py\nimport requests\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get("/orders/{order_id}")\nasync def get_order(order_id):\n r = requests.get(f"https://billing.internal/orders/{order_id}")\n return r.json()\n\n# file: requirements.txt\nfastapi==0.95.2\npydantic==1.10.13',
target: "production",
focus: "async",
context: "Public order API behind an ALB, 300 rps at peak, stores customer addresses, launch in two weeks.",
prescan_facts: {
resources: [{ id: "routes:count", label: "14 routes declared, 9 async" }],
flags: [
{ id: "async:blocking-call", label: "requests.get inside an async def handler" },
{ id: "response-model:missing", label: "7 routes declared without response_model" },
],
},
};
const est = await call("estimate", INPUT);
console.log(est.model, est.model_alias, est.markup_bps);
console.log(est.hold_credits, est.min_credits, est.sponsor_enabled);
// estimate is free: no job is created and nothing is charged. hold_credits is a
// reservation against the output cap; charged_credits is normally far lower.
input := map[string]any{
"files": "# file: app/main.py\nimport requests\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get(\"/orders/{order_id}\")\nasync def get_order(order_id):\n r = requests.get(f\"https://billing.internal/orders/{order_id}\")\n return r.json()\n\n# file: requirements.txt\nfastapi==0.95.2",
"target": "production",
"focus": "async",
"context": "Public order API behind an ALB, 300 rps at peak, stores customer addresses.",
"prescan_facts": map[string]any{
"resources": []any{map[string]string{"id": "routes:count", "label": "14 routes declared, 9 async"}},
"flags": []any{
map[string]string{"id": "async:blocking-call", "label": "requests.get inside an async def handler"},
map[string]string{"id": "response-model:missing", "label": "7 routes without response_model"},
},
},
}
raw, err := call("estimate", input)
if err != nil {
panic(err)
}
fmt.Println(string(raw)) // estimate is free - no job, no charge; the hold is a reservation
String input = """
{
"files": "# file: app/main.py\\nimport requests\\nfrom fastapi import FastAPI\\n\\napp = FastAPI()\\n\\n@app.get(\\"/orders/{order_id}\\")\\nasync def get_order(order_id):\\n r = requests.get(f\\"https://billing.internal/orders/{order_id}\\")\\n return r.json()\\n\\n# file: requirements.txt\\nfastapi==0.95.2",
"target": "production",
"focus": "async",
"context": "Public order API behind an ALB, 300 rps at peak, stores customer addresses.",
"prescan_facts": {
"resources": [
{ "id": "routes:count", "label": "14 routes declared, 9 async" }
],
"flags": [
{ "id": "async:blocking-call", "label": "requests.get inside an async def handler" },
{ "id": "response-model:missing", "label": "7 routes without response_model" }
]
}
}
""";
System.out.println(call("estimate", input));
// estimate is free: no job is created and nothing is charged.
// The data object carries model, model_alias, markup_bps, hold_credits,
// min_credits and sponsor_enabled. hold_credits is a reservation against the
// full output cap, so the settled charge is normally far lower.
input = {
"files" => "# file: app/main.py\nimport requests\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get(\"/orders/{order_id}\")\nasync def get_order(order_id):\n r = requests.get(f\"https://billing.internal/orders/{order_id}\")\n return r.json()\n\n# file: requirements.txt\nfastapi==0.95.2",
"target" => "production",
"focus" => "async",
"context" => "Public order API behind an ALB, 300 rps at peak, stores customer addresses.",
"prescan_facts" => {
"resources" => [{ "id" => "routes:count", "label" => "14 routes declared, 9 async" }],
"flags" => [
{ "id" => "async:blocking-call", "label" => "requests.get inside an async def handler" },
{ "id" => "response-model:missing", "label" => "7 routes without response_model" }
]
}
}
est = call("estimate", input)
puts "#{est['model']} hold=#{est['hold_credits']} min=#{est['min_credits']}"
# estimate is free: no job is created and nothing is charged.
<?php
$input = [
"files" => "# file: app/main.py\nimport requests\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get(\"/orders/{order_id}\")\nasync def get_order(order_id):\n r = requests.get(f\"https://billing.internal/orders/{order_id}\")\n return r.json()\n\n# file: requirements.txt\nfastapi==0.95.2",
"target" => "production",
"focus" => "async",
"context" => "Public order API behind an ALB, 300 rps at peak, stores customer addresses.",
"prescan_facts" => [
"resources" => [["id" => "routes:count", "label" => "14 routes declared, 9 async"]],
"flags" => [
["id" => "async:blocking-call", "label" => "requests.get inside an async def handler"],
["id" => "response-model:missing", "label" => "7 routes without response_model"],
],
],
];
$est = call("estimate", $input);
echo $est["model"], " ", $est["hold_credits"], " ", $est["min_credits"], PHP_EOL;
// estimate is free: no job is created and nothing is charged.
var input = new
{
files = "# file: app/main.py\nimport requests\nfrom fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get(\"/orders/{order_id}\")\nasync def get_order(order_id):\n r = requests.get(f\"https://billing.internal/orders/{order_id}\")\n return r.json()\n\n# file: requirements.txt\nfastapi==0.95.2",
target = "production",
focus = "async",
context = "Public order API behind an ALB, 300 rps at peak, stores customer addresses.",
prescan_facts = new
{
resources = new[] { new { id = "routes:count", label = "14 routes declared, 9 async" } },
flags = new[]
{
new { id = "async:blocking-call", label = "requests.get inside an async def handler" },
new { id = "response-model:missing", label = "7 routes without response_model" }
}
}
};
var est = await Clinic.Call("estimate", input);
Console.WriteLine(est.GetProperty("hold_credits").GetInt32());
Console.WriteLine(est.GetProperty("sponsor_enabled").GetBoolean());
// estimate is free: no job is created and nothing is charged. The hold is a
// reservation against the output cap, not the price of the run.
5. Run it, then poll
POST /run returns a job_id; poll GET jobs/{job_id} until
status is succeeded or failed. The review JSON is the string
at data.output.output.
Always send an Idempotency-Key. Derive it from the input, as the web
app does (fastapi-clinic:<hash>:a<attempt>). A retried request carrying the
same key returns the same job instead of billing a second run — which is what makes a CI retry
safe. Replaying a key with a different body is a 409 conflict, so bump the
attempt suffix whenever the input actually changed.
# 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.
KEY="fastapi-clinic:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "X-App-Slug: $SLUG" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
# Poll until the job reaches a terminal status.
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
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])'
import hashlib, time
# 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.
digest = hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:16]
key = f"fastapi-clinic:{digest}:a1"
req = urllib.request.Request(f"{BASE}/run", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("X-App-Slug", SLUG)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call(f"jobs/{job_id}")
if job["status"] == "succeeded":
break
if job["status"] == "failed":
raise RuntimeError(job.get("error"))
time.sleep(2)
review = json.loads(job["output"]["output"])
print(review["readiness"], review["fastapi_version"], len(review["findings"]), "findings")
import { createHash } from "node:crypto";
// 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.
const digest = createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16);
const key = `fastapi-clinic:${digest}:a1`;
const started = await fetch(`${BASE}/run`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"X-App-Slug": SLUG,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify(INPUT),
}).then((r) => r.json());
let job = started.data;
while (job.status !== "succeeded" && job.status !== "failed") {
await new Promise((r) => setTimeout(r, 2000));
job = await call(`jobs/${job.job_id}`);
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const review = JSON.parse(job.output.output);
console.log(review.readiness, review.app_shape, review.findings.length, "findings");
// 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.
body, _ := json.Marshal(input)
sum := sha256.Sum256(body)
key := fmt.Sprintf("fastapi-clinic:%x:a1", sum[:8])
req, _ := http.NewRequest(http.MethodPost, base+"/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", slug)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var started struct {
Data struct {
JobID string `json:"job_id"`
} `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&started)
for {
raw, err := call("jobs/"+started.Data.JobID, nil)
if err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
_ = json.Unmarshal(raw, &job)
if job.Status == "succeeded" {
fmt.Println(job.Output.Output) // the review JSON, as a string
break
}
if job.Status == "failed" {
panic("run failed")
}
time.Sleep(2 * time.Second)
}
// 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.
var digest = java.security.MessageDigest.getInstance("SHA-256")
.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8));
var key = "fastapi-clinic:" + java.util.HexFormat.of().formatHex(digest).substring(0, 16) + ":a1";
var start = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("X-App-Slug", SLUG)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(input))
.build();
String started = HTTP.send(start, HttpResponse.BodyHandlers.ofString()).body();
// Parse job_id out of `started`, then poll GET jobs/{job_id} every two seconds
// until status is "succeeded" or "failed"; the review JSON is data.output.output.
System.out.println(started);
require "digest"
# 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.
digest = Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]
key = "fastapi-clinic:#{digest}:a1"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = JSON.generate(input)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]
loop do
job = call("jobs/#{job_id}")
break puts(job["output"]["output"]) if job["status"] == "succeeded"
raise "run failed" if job["status"] == "failed"
sleep 2
end
<?php
// 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.
$digest = substr(hash("sha256", json_encode($input)), 0, 16);
$key = "fastapi-clinic:{$digest}:a1";
$ch = curl_init(BASE . "/run");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($input));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . TOKEN,
"X-App-Slug: " . SLUG,
"Content-Type: application/json",
"Idempotency-Key: " . $key,
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);
while (true) {
$job = call("jobs/" . $jobId);
if ($job["status"] === "succeeded") { echo $job["output"]["output"]; break; }
if ($job["status"] === "failed") { throw new RuntimeException("run failed"); }
sleep(2);
}
using System.Security.Cryptography;
using System.Text;
// 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.
var json = JsonSerializer.Serialize(input);
var digest = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json)))[..16].ToLowerInvariant();
var key = $"fastapi-clinic:{digest}:a1";
var run = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run");
run.Headers.Add("Authorization", "Bearer YOUR_TOKEN");
run.Headers.Add("X-App-Slug", "fastapi-clinic");
run.Headers.Add("Idempotency-Key", key);
run.Content = JsonContent.Create(input);
// POST it, read data.job_id, then poll GET jobs/{job_id} every two seconds until
// status is "succeeded" or "failed"; the review JSON is data.output.output.
6. Or stream it
POST /run-stream is the same call over server-sent events. The web app uses it to
advance a staged progress display as sections arrive, and to keep whatever parsed if the stream
dies mid-flight. The final done event carries charged_credits — the real
price, normally a fraction of the hold — and the truncated flag.
# Server-sent events. Each `delta` carries a chunk of the JSON review; the final
# `done` event carries the status, charged_credits and the truncated flag.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "X-App-Slug: $SLUG" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d "$INPUT"
# event: job {"job_id":"job_..."}
# event: delta {"text":"{\"review_name\":\"orders-api"}
# event: delta {"text":" — production readiness review\","}
# event: done {"status":"succeeded","charged_credits":451,"truncated":false}
# Server-sent events: the review arrives in chunks, so a UI can show progress.
req = urllib.request.Request(f"{BASE}/run-stream", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("X-App-Slug", SLUG)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
req.add_header("Accept", "text/event-stream")
raw = ""
done = {}
event = None
with urllib.request.urlopen(req) as stream:
for line in stream:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
raw += json.loads(line[6:]).get("text", "")
elif line.startswith("data: ") and event == "done":
done = json.loads(line[6:])
review = json.loads(raw[raw.index("{"):raw.rindex("}") + 1])
print(review["readiness"], len(review["findings"]), "findings", done.get("charged_credits"))
// Server-sent events: the review arrives in chunks, so a UI can show progress.
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"X-App-Slug": SLUG,
"Content-Type": "application/json",
"Idempotency-Key": key,
Accept: "text/event-stream",
},
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let raw = "";
let done = {};
let event = null;
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ") && event === "delta") {
raw += JSON.parse(line.slice(6)).text ?? "";
} else if (line.startsWith("data: ") && event === "done") {
done = JSON.parse(line.slice(6));
}
}
}
const review = JSON.parse(raw.slice(raw.indexOf("{"), raw.lastIndexOf("}") + 1));
console.log(review.readiness, review.findings.length, "findings", done.charged_credits);
// Server-sent events: the review arrives in chunks, so a UI can show progress.
req, _ = http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", slug)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, _ = http.DefaultClient.Do(req)
defer res.Body.Close()
var raw strings.Builder
var event string
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimPrefix(line, "event: ")
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct {
Text string `json:"text"`
}
_ = json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &d)
raw.WriteString(d.Text)
case strings.HasPrefix(line, "data: ") && event == "done":
fmt.Println(strings.TrimPrefix(line, "data: ")) // status, charged_credits, truncated
}
}
fmt.Println(raw.String())
// Server-sent events: the review arrives in chunks, so a UI can show progress.
var stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("X-App-Slug", SLUG)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(input))
.build();
StringBuilder raw = new StringBuilder();
String[] event = { null };
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.startsWith("event: ")) event[0] = line.substring(7);
else if (line.startsWith("data: ") && "delta".equals(event[0])) {
raw.append(line.substring(6)); // each data line is {"text":"..."} - decode and append .text
}
});
System.out.println(raw);
// The final `done` event carries status, charged_credits and truncated.
# Server-sent events: the review arrives in chunks, so a UI can show progress.
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req["Accept"] = "text/event-stream"
req.body = JSON.generate(input)
raw = +""
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.chomp
if line.start_with?("event: ") then event = line[7..]
elsif line.start_with?("data: ") && event == "delta"
raw << (JSON.parse(line[6..])["text"] || "")
end
end
end
end
end
review = JSON.parse(raw[raw.index("{")..raw.rindex("}")])
puts "#{review['readiness']} #{review['findings'].length} findings"
<?php
// Server-sent events: the review arrives in chunks, so a UI can show progress.
$raw = "";
$event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($input));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . TOKEN,
"X-App-Slug: " . SLUG,
"Content-Type: application/json",
"Idempotency-Key: " . $key,
"Accept: text/event-stream",
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) {
$event = substr($line, 7);
} elseif (str_starts_with($line, "data: ") && $event === "delta") {
$raw .= json_decode(substr($line, 6), true)["text"] ?? "";
}
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
$review = json_decode(substr($raw, strpos($raw, "{")), true);
echo $review["readiness"], PHP_EOL;
// Server-sent events: the review arrives in chunks, so a UI can show progress.
var stream = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream");
stream.Headers.Add("Authorization", "Bearer YOUR_TOKEN");
stream.Headers.Add("X-App-Slug", "fastapi-clinic");
stream.Headers.Add("Idempotency-Key", key);
stream.Headers.Add("Accept", "text/event-stream");
stream.Content = JsonContent.Create(input);
using var res = await Http.SendAsync(stream, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string? evt = null;
while (await reader.ReadLineAsync() is { } line)
{
if (line.StartsWith("event: ")) evt = line[7..];
else if (line.StartsWith("data: ") && evt == "delta")
{
var d = JsonSerializer.Deserialize<JsonElement>(line[6..]);
if (d.TryGetProperty("text", out var t)) raw.Append(t.GetString());
}
}
Console.WriteLine(raw.ToString());
7. Parse the review and check the reconciliation
Two invariants are worth enforcing on your side, because the app enforces them too: every
focus_areas[].finding_ids entry must name a real finding id, and every
prescan_facts.flags id must appear exactly once in
coverage_check. A flag missing from the reconciliation means the model quietly skipped
a fact your own static analyzer established — treat that as a failed run, not a passing one, and
retry with a retry_note naming the missing ids. An entry with
addressed: false is fine: that is the model deliberately setting a flag aside, with
the reason in note, which is a different thing from silence.
The truncated flag on the done event (and on the finished job) means the
reply hit the output cap. What you hold is a prefix, not a review: retry with a
retry_note asking for fewer, denser findings rather than trying to repair the JSON.
# The review JSON is a string inside the envelope, so unwrap it twice.
REVIEW=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])')
printf '%s' "$REVIEW" | python3 -c '
import sys, json
r = json.load(sys.stdin)
print(r["readiness"], "|", r["verdict"])
print(r["fastapi_version"], "pydantic", r["pydantic_major"], "|", r["app_shape"])
for c in r["checks"]:
print(" %-8s %s" % (c["status"], c["check"]))
for f in r["findings"]:
print(" %s %-8s %-16s %s" % (f["id"], f["priority"], f["category"], f["resource"]))
'
# Every prescan flag id must come back exactly once in coverage_check.
printf '%s' "$REVIEW" | python3 -c '
import sys, json
seen = [c["id"] for c in json.load(sys.stdin)["coverage_check"]]
want = ["async:blocking-call", "response-model:missing"]
missing = [i for i in want if seen.count(i) != 1]
if missing:
raise SystemExit("unreconciled prescan flags: " + ", ".join(missing))
print("coverage_check reconciles")
'
review = json.loads(job["output"]["output"])
# 1. Every prescan flag id appears exactly once in coverage_check.
sent = [f["id"] for f in INPUT["prescan_facts"]["flags"]]
seen = [c["id"] for c in review["coverage_check"]]
missing = [i for i in sent if seen.count(i) != 1]
extra = [i for i in seen if i not in sent]
if missing or extra:
raise RuntimeError(f"coverage_check drift: missing={missing} extra={extra}")
# 2. Every focus_areas finding id exists in findings.
ids = {f["id"] for f in review["findings"]}
for area in review["focus_areas"]:
for fid in area["finding_ids"]:
if fid not in ids:
raise RuntimeError(f"focus_areas references unknown finding {fid}")
# 3. A truncated reply is a prefix, not a review. Retry, do not repair.
if job.get("truncated"):
INPUT["retry_note"] = (
"The previous reply was truncated. Return the same twelve checks but at most "
"eight findings, each with a shorter snippet."
)
# ... resubmit with an incremented attempt suffix in the Idempotency-Key.
for c in review["checks"]:
print(f"{c['status']:8} {c['check']:32} {c['evidence']}")
for f in review["findings"]:
print(f["id"], f["priority"], f["category"], f["resource"])
print(review["refactored_route"])
print("\n".join(review["commands"]))
const review = JSON.parse(job.output.output);
// 1. Every prescan flag id appears exactly once in coverage_check.
const sent = INPUT.prescan_facts.flags.map((f) => f.id);
const seen = review.coverage_check.map((c) => c.id);
const missing = sent.filter((id) => seen.filter((s) => s === id).length !== 1);
const extra = seen.filter((id) => !sent.includes(id));
if (missing.length || extra.length) {
throw new Error(`coverage_check drift: missing=${missing} extra=${extra}`);
}
// 2. Every focus_areas finding id exists in findings.
const ids = new Set(review.findings.map((f) => f.id));
for (const area of review.focus_areas) {
for (const fid of area.finding_ids) {
if (!ids.has(fid)) throw new Error(`focus_areas references unknown finding ${fid}`);
}
}
// 3. A truncated reply is a prefix, not a review. Retry, do not repair.
if (job.truncated) {
INPUT.retry_note =
"The previous reply was truncated. Return the same twelve checks but at most eight findings.";
}
for (const c of review.checks) console.log(c.status.padEnd(8), c.check, "—", c.evidence);
for (const f of review.findings) console.log(f.id, f.priority, f.category, f.resource);
console.log(review.refactored_route);
console.log(review.commands.join("\n"));
type check struct {
Check string `json:"check"`
Status string `json:"status"`
Evidence string `json:"evidence"`
Requirement string `json:"requirement"`
}
type finding struct {
ID string `json:"id"`
Category string `json:"category"`
Severity string `json:"severity"`
Priority string `json:"priority"`
Resource string `json:"resource"`
Problem string `json:"problem"`
Fix string `json:"fix"`
Snippet string `json:"snippet"`
}
type review struct {
Readiness string `json:"readiness"`
Verdict string `json:"verdict"`
FastAPIVersion string `json:"fastapi_version"`
PydanticMajor string `json:"pydantic_major"`
AppShape string `json:"app_shape"`
Checks []check `json:"checks"`
Findings []finding `json:"findings"`
RefactoredRoute string `json:"refactored_route"`
Commands []string `json:"commands"`
CoverageCheck []struct {
ID string `json:"id"`
Addressed bool `json:"addressed"`
Note string `json:"note"`
} `json:"coverage_check"`
}
var r review
if err := json.Unmarshal([]byte(job.Output.Output), &r); err != nil {
panic(err)
}
// Every prescan flag id must come back exactly once in coverage_check.
count := map[string]int{}
for _, c := range r.CoverageCheck {
count[c.ID]++
}
for _, id := range []string{"async:blocking-call", "response-model:missing"} {
if count[id] != 1 {
panic("unreconciled prescan flag: " + id)
}
}
fmt.Println(r.Readiness, r.FastAPIVersion, r.PydanticMajor, len(r.Findings), "findings")
// The review JSON is a string inside data.output.output - parse it, then check
// the two invariants before you trust it:
//
// 1. every prescan_facts.flags id appears exactly once in coverage_check;
// 2. every focus_areas[].finding_ids entry names an id present in findings.
//
// A `truncated` job is a prefix, not a review: resubmit with a retry_note such as
// "The previous reply was truncated. Return the same twelve checks but at most
// eight findings" and an incremented attempt suffix on the Idempotency-Key.
String reviewJson = /* data.output.output */ call("jobs/" + jobId, null);
System.out.println(reviewJson);
// checks[] is always the same twelve entries in the same order, so a table can
// be rendered by index without searching for a check by name.
review = JSON.parse(job["output"]["output"])
# 1. Every prescan flag id appears exactly once in coverage_check.
sent = input["prescan_facts"]["flags"].map { |f| f["id"] }
seen = review["coverage_check"].map { |c| c["id"] }
missing = sent.reject { |id| seen.count(id) == 1 }
extra = seen - sent
raise "coverage_check drift: #{missing} / #{extra}" unless missing.empty? && extra.empty?
# 2. Every focus_areas finding id exists in findings.
ids = review["findings"].map { |f| f["id"] }
review["focus_areas"].each do |area|
area["finding_ids"].each { |fid| raise "unknown finding #{fid}" unless ids.include?(fid) }
end
review["checks"].each { |c| puts format("%-8s %s", c["status"], c["check"]) }
review["findings"].each { |f| puts "#{f['id']} #{f['priority']} #{f['category']} #{f['resource']}" }
puts review["refactored_route"]
puts review["commands"].join("\n")
<?php
$review = json_decode($job["output"]["output"], true);
// 1. Every prescan flag id appears exactly once in coverage_check.
$sent = array_column($input["prescan_facts"]["flags"], "id");
$seen = array_column($review["coverage_check"], "id");
$counts = array_count_values($seen);
foreach ($sent as $id) {
if (($counts[$id] ?? 0) !== 1) {
throw new RuntimeException("unreconciled prescan flag: " . $id);
}
}
// 2. Every focus_areas finding id exists in findings.
$ids = array_column($review["findings"], "id");
foreach ($review["focus_areas"] as $area) {
foreach ($area["finding_ids"] as $fid) {
if (!in_array($fid, $ids, true)) {
throw new RuntimeException("focus_areas references unknown finding " . $fid);
}
}
}
foreach ($review["checks"] as $c) {
printf("%-8s %s\n", $c["status"], $c["check"]);
}
echo $review["refactored_route"], PHP_EOL;
var review = JsonSerializer.Deserialize<JsonElement>(reviewJson);
// 1. Every prescan flag id appears exactly once in coverage_check.
var seen = review.GetProperty("coverage_check")
.EnumerateArray()
.Select(c => c.GetProperty("id").GetString())
.ToList();
foreach (var id in new[] { "async:blocking-call", "response-model:missing" })
{
if (seen.Count(s => s == id) != 1)
throw new Exception($"unreconciled prescan flag: {id}");
}
// 2. Every focus_areas finding id exists in findings.
var ids = review.GetProperty("findings")
.EnumerateArray()
.Select(f => f.GetProperty("id").GetString())
.ToHashSet();
foreach (var area in review.GetProperty("focus_areas").EnumerateArray())
foreach (var fid in area.GetProperty("finding_ids").EnumerateArray())
if (!ids.Contains(fid.GetString()))
throw new Exception($"focus_areas references unknown finding {fid}");
foreach (var c in review.GetProperty("checks").EnumerateArray())
Console.WriteLine($"{c.GetProperty("status")} {c.GetProperty("check")}");
The output contract
data.output.output is a JSON string holding one object. This is exactly what the web
app parses, so anything that renders here will render there:
{
"review_name": "orders-api — production readiness review",
"readiness": "production-ready | needs-work | not-ready",
"verdict": "one sentence naming the single thing that decides the readiness",
"fastapi_version": "0.95.2",
"pydantic_major": "1",
"app_shape": "single-module app, 14 routes, no router split",
"reviewed_against": "production | staging | internal | unknown",
"exec_summary": "2-3 paragraphs separated by blank lines",
"assumptions": ["..."],
"open_questions": ["..."],
"inventory": [
{ "kind": "Route", "name": "GET /orders/{order_id}", "value": "async def get_order",
"role": "what it does here" }
],
"checks": [
{ "check": "Async correctness", "status": "fail",
"evidence": "requests.get called inside async def get_order at app/main.py",
"requirement": "No blocking I/O on the event loop: use httpx.AsyncClient or def" }
],
"findings": [
{
"id": "FA-001",
"category": "async | dependencies | validation | response-model | error-handling | auth | database | cors | performance | configuration | testing | observability",
"severity": "low | medium | high",
"likelihood": "low | medium | high",
"priority": "critical | high | medium | low",
"resource": "Route/GET /orders/{order_id}",
"problem": "...",
"impact": "...",
"fix": "...",
"snippet": "corrected Python fragment, or \"\""
}
],
"coverage_check": [
{ "id": "async:blocking-call", "addressed": true, "note": "FA-001." }
],
"refactored_route": "one corrected route, rewritten end to end, as a JSON string",
"commands": ["uvicorn app.main:app --workers 4 # run it the way production will"],
"quick_wins": ["..."],
"focus_areas": [{ "area": "...", "why": "...", "finding_ids": ["FA-001"] }],
"summary": "closing paragraph"
}
Every key
| key | type | meaning |
|---|---|---|
review_name | string | Short title naming the service and the target it was reviewed against. |
readiness | enum | production-ready, needs-work or not-ready. The single value a CI gate should branch on. |
verdict | string | One sentence justifying the readiness and naming the thing that decides it. |
fastapi_version | string | The version the paste shows, e.g. "0.115.0", or "unknown". Never guessed. |
pydantic_major | string | "1", "2" or "unknown". It decides which idioms the fixes are written in — @validator versus @field_validator, .dict() versus .model_dump(). |
app_shape | string | How the project is put together: single module, routers, factory function, how many routes, whether there is a lifespan handler. |
reviewed_against | enum | Echoes the target reviewed against: production, staging, internal, unknown. |
exec_summary | string | Two to three paragraphs on the dominant themes, separated by blank lines. |
assumptions | string[] | Explicit assumptions filling gaps in the paste — how many workers run, whether a gateway terminates TLS, which database driver is in use. |
open_questions | string[] | Questions whose answers would change the review or its ordering. |
inventory | object[] | {kind, name, value, role}. kind is one of Route, Router, Dependency, Model, Middleware, Setting, Session, BackgroundTask, Package. |
checks | object[] | {check, status, evidence, requirement}. Always the same twelve checks in the same fixed order — render by index, do not search by name. |
findings | object[] | {id, category, severity, likelihood, priority, resource, problem, impact, fix, snippet}. Ids are sequential FA-001, FA-002, … Always at least one entry. snippet is a pasteable Python fragment or "". |
coverage_check | object[] | {id, addressed, note}. One entry per prescan_facts.flags id, exactly once, and no ids the prescan did not send. addressed: false means deliberately set aside, with the reason in note. |
refactored_route | string | One route rewritten end to end — decorator, signature, dependencies, response model, error handling — as a JSON string. The route chosen is the one carrying the most findings. "" when no route was pasted. Never contains a real secret. |
commands | string[] | Ordered shell commands, each with a trailing comment. Read-only — nothing that deletes, migrates or deploys. |
quick_wins | string[] | One-line changes worth doing immediately. |
focus_areas | object[] | {area, why, finding_ids}. Every id in finding_ids must exist in findings. |
summary | string | Closing paragraph: what to do first and what remains after that. |
The enums
| field | values | notes |
|---|---|---|
readiness | production-ready, needs-work, not-ready | production-ready: the twelve checks hold as a set and the rest is improvement work. needs-work: named checks fail or are partial, but nothing breaks under the stated traffic today. not-ready: at least one finding means this falls over, leaks data or blocks the event loop under the traffic described in context. |
findings[].category | async, dependencies, validation, response-model, error-handling, auth, database, cors, performance, configuration, testing, observability | validation and response-model are separate on purpose: an unvalidated request body and a route leaking a full ORM object are different problems with different fixes. |
findings[].severityfindings[].likelihood | low, medium, high | Severity is how bad it is when it happens; likelihood is how reachable it is from the code as pasted, at the traffic described in context. |
findings[].priority | critical, high, medium, low | Severity by likelihood, adjusted for target. critical is reserved for something that breaks or exposes the service now on a production deployment: blocking I/O on the event loop of a public route, a database session leaked per request, a route returning an ORM object with password or token fields, a committed secret, or an unauthenticated route returning another user's records. |
checks[].status | pass, fail, partial, unknown | unknown is a legitimate answer when the paste does not show enough to decide, and is preferred over a guess. partial means the practice is present but incomplete — response models on some routes, a global exception handler that swallows the cause. |
The twelve checks
checks always carries these twelve, in this order, on every run — so a table can be
rendered by index and two reviews of the same project are diffable row by row:
1. Async correctness 7. Authentication and secrets
2. Dependency injection 8. Database session lifecycle
3. Request validation 9. CORS and middleware
4. Response models 10. Pagination and payload limits
5. Status codes 11. Configuration
6. Error handling 12. Testing and observability
The review never echoes a secret value. If the paste contains a literal API key, a database URL
with a password or a JWT signing secret, the finding names the setting and says to rotate it — the
value itself does not appear in problem, snippet or
refactored_route.
A CI gate
The readiness value is the natural exit code. Fail the job when a service drifts into
not-ready, warn on needs-work, and pass on production-ready —
with the Idempotency-Key derived from the input so a re-run of the same commit replays instead of
re-billing.
READINESS=$(printf '%s' "$REVIEW" | python3 -c 'import sys,json;print(json.load(sys.stdin)["readiness"])')
CRITICAL=$(printf '%s' "$REVIEW" | python3 -c 'import sys,json;print(sum(1 for f in json.load(sys.stdin)["findings"] if f["priority"]=="critical"))')
case "$READINESS" in
not-ready) echo "::error::FastAPI review: not ready ($CRITICAL critical)"; exit 1 ;;
needs-work) echo "::warning::FastAPI review: needs work"; exit 0 ;;
production-ready) echo "FastAPI review: production ready"; exit 0 ;;
esac