ORM Query Check API
ORM Query Check has two parts. The checker is free and runs only in your browser:
pqscan.js parses schema.prisma and checks every Prisma Client call in pasted code
against it (unique selectors, fields, relations, provider support, raw SQL, transactions, queries in loops).
It makes no API call and costs nothing, so it is not part of this API — the file is plain JavaScript you
can run yourself.
The two metered lanes are what the API runs. Each one is a single model run that returns one JSON object:
- write — a plain-English data request becomes one correct Prisma Client call (or a short sequence) that uses only the models and fields in your schema.
- review — your Prisma Client code plus the checker's findings becomes an answer to every finding, the problems the checker missed, and the fixed code.
Everything below is taken from the page's own source: sdk.js (endpoints, headers, envelope),
kit.js (the run input), SKILL.md (the model's instructions) and recon.js
(how the reply is read). The web page runs the reply back through the checker; over the API you get the reply
as it is, so run pqscan.js on returned code if you want the same check.
The task field comes first
Every request body is one JSON object, and its task field picks the lane. There are exactly two
lanes: "write" and "review". The model is told to honour task; if it is
missing or unrecognised it picks the closer lane from the fields present and says which in the reply's own
task field. The two reply shapes are never mixed.
| task | What it does | Send | Reply keys |
|---|---|---|---|
write | Writes a query from a request. | task, schema, request, facts; optional note | headline, method, model, code, explanation, returns, fields_used, assumptions, caveats, cannot |
review | Reviews code you already wrote. | task, schema, code, facts (with the checker's findings and calls); optional note | headline, verdict, finding_responses, extra_findings, fixed_code, changes, to_confirm |
Input fields
The page builds this object with QueryKit.buildInput(lane, analysis, {note}). The limits are the
page's own clipping rules from kit.js; the SDK documents no server-side limits.
| Field | Type | Lane | Meaning | Page clipping limit |
|---|---|---|---|---|
task | string | both | "write" or "review". | — |
schema | string | both | Your whole schema.prisma as text. | 30,000 characters. A longer schema is cut down to the generator, datasource and enum blocks plus the models the lane needs (named in the request for write, called in the code for review) and their direct relations, capped at 30,000, then a comment // [... ORM Query Check cut: N models not sent ...] naming the models left out. |
request | string | write | What you want, in plain words (the page asks for at least 8 characters). | 3,000 characters: the opening and the closing are kept and the middle is replaced by [... cut by ORM Query Check: N characters from the middle]. |
code | string | review | TypeScript or JavaScript that calls Prisma Client (prisma.user.findMany(...)). | 24,000 characters. Longer code keeps the lines around every Prisma call (8 before, 16 after) and 8 lines either side of $transaction, $queryRaw, $executeRaw and PrismaClient; each gap becomes // [... ORM Query Check cut: lines A-B not sent ...], and the result is capped at 36,000. |
facts | string (JSON) | both | A JSON object serialised as a string, written by the browser checker. See the next table. | findings: first 60; calls: first 80; schema_problems: first 20. |
note | string | both, optional | Context from you (framework, what goes wrong). Sent only when not empty. | 2,000 characters, cut the same way as request. |
retry_note | string | both, optional | Not typed by the user. The page adds it on its one automatic retry, when a reply was not the single valid JSON object the instructions require; that retry is a new run with attempt a2. | Not clipped. |
The facts string
facts is JSON.stringify of the object kit.js facts() returns. It is what
the model trusts about your schema, and in review it carries the findings the reply must answer by id. If you call
the API without the page, build the same object (running pqscan.js gives you every value) and send it as a string.
| Key | Lane | Value |
|---|---|---|
provider | both | The datasource provider, e.g. postgresql, mysql; unknown when absent. |
generator | both | prisma-client or prisma-client-js; unknown when absent. |
client_api | both | prisma-orm-7 (prisma-client generator), prisma-orm-6 or 7 (prisma-client-js generator) or unknown. |
capabilities | both | Object: provider, mode_insensitive, and_return, skip_duplicates, json_filters (array path like ['a','b'], string path like '$.a.b' or not supported), scalar_lists, is_set. Booleans are null when there is no provider. |
models | both | Array of {model, accessor, fields, unique}. fields are name:Type with [] for lists, ? for optional and (relation), (relation, list) or (enum) markers; unique lists the selectors (id, email, compound orderId_productId). When the schema was clipped, only the models sent. |
enums | both | Object: enum name to its values. |
schema_problems | both | Up to 20 strings, line N: text. |
findings | review | Up to 60 of {id, severity, code, line, quote, text}: ids F1, F2, ...; severity error, warning or info; code is the rule. |
findings_not_sent | review, when over 60 | How many findings were left out. |
calls | review | Up to 80 of {call, line}, e.g. {"call": "prisma.customer.findUnique", "line": 6}. |
status | review | The checker's verdict on the code: unsafe, errors, warnings or clean. |
schema_clipped | when clipped | {sent, models_not_sent}. |
code_clipped | review, when clipped | {lines_not_sent, total_chars}. |
The output contract
A finished job carries the model's reply as text at job.output.output. By the rules in
SKILL.md that text is one JSON object with every key present (empty arrays when there is nothing to say)
and no prose or code fence around it. The page parses it with recon.js: parseResult takes the
first balanced JSON object in the text (tolerating a stray fence), and normalize(obj, lane) gives each
lane one shape, adding lane, lane_mismatch and present (the section keys the reply
actually carried). The rightmost column is what normalize does to each key.
task "write"
| Key | Type | Meaning (SKILL.md) | recon.js normalize |
|---|---|---|---|
task | string | "write". | Picks the shape; an unknown value falls back to the lane you asked for, and a known other lane is flagged as lane_mismatch. |
headline | string | One sentence: what the call does. | Stringified. |
method | string | The main call, e.g. findMany. | Trimmed. |
model | string | The main model, e.g. Post. | Trimmed. |
code | string | TypeScript; "" when the request cannot be met. | A wrapping code fence is removed; trailing space trimmed. |
explanation | string[] | One short line per decision. | Non-empty strings, first 30. |
returns | string | The shape the call resolves to. | Stringified. |
fields_used | string[] | Every Model.field the code touches. | First 80. The page checks each one exists. |
assumptions | string[] | What had to be assumed. | First 20. |
caveats | {kind, text}[] | kind: performance, correctness, database or security. | First 20; an unknown kind becomes correctness; empty text dropped. |
cannot | string | Empty, or one or two sentences naming exactly what the schema lacks. | Trimmed; non-empty sets the page's verdict to cannot, else written. |
task "review"
| Key | Type | Meaning (SKILL.md) | recon.js normalize |
|---|---|---|---|
task | string | "review". | As for write. |
headline | string | One-sentence verdict. | Stringified. |
verdict | string | ok, fix or unsafe (SQL injection, or a query that can hit every row). | Lower-cased; anything else becomes fix, the raw value kept as verdict_raw. |
finding_responses | {ref, stance, note}[] | Every facts.findings id answered once; stance confirmed or not_applicable. | First 200; ref (or id) upper-cased; an unknown stance becomes unclear, raw kept as stance_raw. |
extra_findings | {title, severity, line, evidence, problem, fix}[] | Problems the checker did not report; evidence is copied verbatim from code. | First 40; severity error/warning/info, default warning; line a number, 0 when missing. |
fixed_code | string | The whole code with every fix; "" when the verdict is ok. | Code fence removed; trailing space trimmed. |
changes | string[] | One line per change, e.g. line 4: findUnique -> findFirst .... | First 60. |
to_confirm | string[] | Questions you must answer. | First 20. |
Worked examples
Both are real runs the page ships as saved examples. The long schema and facts strings are
abbreviated here (marked ... [abbreviated: N characters in full]); every other value is exact.
write: a paged, case-insensitive search (PostgreSQL)
The schema has User, Post, Comment and Tag and a Role enum on
postgresql with the prisma-client generator; facts says mode_insensitive: true.
Input:
{
"task": "write",
"schema": "generator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n}\n\n ... [abbreviated: 1298 characters in full]",
"facts": "{\"provider\":\"postgresql\",\"generator\":\"prisma-client\",\"client_api\":\"prisma-orm-7 (prisma-cl ... [abbreviated: 1215 characters in full]",
"request": "Search published posts whose title contains a search term, ignoring case. Show 20 per page, newest first by publish date, with the author's name, the tag names, and how many comments each post has. I page with a cursor (the last post id I got)."
}
Reply (the text at job.output.output, parsed and pretty-printed):
{
"task": "write",
"headline": "Finds up to 20 published posts whose title matches a search term (case-insensitive), newest-published first, with the author's name, tag names, and a comment count, paged by a post-id cursor.",
"method": "findMany",
"model": "Post",
"code": "declare const searchTerm: string;\ndeclare const cursorId: number | undefined;\n\nconst posts = await prisma.post.findMany({\n where: {\n published: true,\n title: { contains: searchTerm, mode: 'insensitive' },\n },\n orderBy: [\n { publishedAt: { sort: 'desc', nulls: 'last' } },\n { id: 'desc' },\n ],\n take: 20,\n ...(cursorId !== undefined ? { cursor: { id: cursorId }, skip: 1 } : {}),\n select: {\n id: true,\n title: true,\n publishedAt: true,\n author: { select: { name: true } },\n tags: { select: { name: true } },\n _count: { select: { comments: true } },\n },\n});",
"explanation": [
"where combines published: true with title: { contains: searchTerm, mode: 'insensitive' } for a case-insensitive substring match; mode is valid because facts.provider is postgresql.",
"orderBy sorts by publishedAt descending for \"newest first by publish date\", with id descending as a tiebreaker so ties don't reorder between pages; nulls: 'last' keeps a published post with no publishedAt from sorting as newest.",
"cursor: { id: cursorId }, skip: 1 resumes after the last post id the caller already saw; it is applied only when cursorId is defined, so the first page (no prior id) just returns the first 20 rows.",
"select returns only what a results list needs - title, publish date, author name, tag names - instead of the full post body.",
"_count: { select: { comments: true } } gives the comment total for each post without loading the comment rows."
],
"returns": "Array<{ id: number; title: string; publishedAt: Date | null; author: { name: string | null }; tags: { name: string }[]; _count: { comments: number } }>",
"fields_used": [
"Post.published",
"Post.title",
"Post.publishedAt",
"Post.id",
"Post.author",
"Post.tags",
"Post.comments",
"User.name",
"Tag.name"
],
"assumptions": [
"\"Newest first by publish date\" sorts by Post.publishedAt, not createdAt, since publishedAt is the field that represents when the post went live.",
"cursorId is the last post's id from the previous page, supplied by the caller as undefined on the first page.",
"Only a results-list view is needed (title, publish date, author name, tag names, comment count), not the full post body."
],
"caveats": [
{
"kind": "correctness",
"text": "Post.publishedAt is optional, so a row with published: true but publishedAt: null (allowed by the schema even if the app never creates one) sorts to the end under nulls: 'last' instead of appearing as 'newest'."
},
{
"kind": "database",
"text": "mode: 'insensitive' depends on facts.provider being postgresql; it must be dropped if this query is ever run against MySQL or SQL Server."
},
{
"kind": "performance",
"text": "title: { contains } can't use a plain B-tree index, so on a large posts table add a pg_trgm trigram index (or a full-text index) on title to keep the search fast."
}
],
"cannot": ""
}
review: a MySQL shop with eight checker findings
The schema is Customer, Product, Order and OrderItem (compound
@@id([orderId, productId])) on mysql. facts.findings carries F1-F8 from the browser
checker: unique-where (line 6), db-support (11), include-scalar (12),
bad-value (13), raw-injection (18), tx-outer-client (25) and two
query-in-loop warnings (25, 26); facts.status is unsafe.
Input:
{
"task": "review",
"schema": "generator client {\n provider = \"prisma-client-js\"\n}\n\ndatasource db {\n provider = \"mysql\" ... [abbreviated: 1138 characters in full]",
"facts": "{\"provider\":\"mysql\",\"generator\":\"prisma-client-js\",\"client_api\":\"prisma-orm-6 or 7 (prisma ... [abbreviated: 3470 characters in full]",
"code": "import { PrismaClient } from '@prisma/client'\n\nconst prisma = new PrismaClient()\n\nexport async function customerByName(name: string) {\n return prisma.customer.findUnique({ where: { name } })\n}\n\nexport async function searchProducts(term: string) {\n return prisma.product.findMany({\n where: { name: { contains: term, mode: 'insensitive' } },\n include: { sku: true },\n orderBy: { price: 'ASC' },\n })\n}\n\nexport async function ordersForEmail(email: string) {\n return prisma.$queryRawUnsafe(`SELECT * FROM \\`Order\\` o JOIN Customer c ON c.id = o.customerId WHERE c.email = '${email}'`)\n}\n\nexport async function placeOrder(customerId: number, lines: { productId: number; quantity: number }[]) {\n return prisma.$transaction(async (tx) => {\n const order = await tx.order.create({ data: { customerId, total: 0 } })\n for (const line of lines) {\n await prisma.product.update({ where: { id: line.productId }, data: { stock: { decrement: line.quantity } } })\n await tx.orderItem.create({ data: { orderId: order.id, productId: line.productId, quantity: line.quantity } })\n }\n return order\n })\n}\n",
"note": "Express API on MySQL. placeOrder is slow and sometimes leaves stock wrong."
}
Reply:
{
"task": "review",
"headline": "Unsafe: ordersForEmail builds raw SQL from a template string ($queryRawUnsafe), and placeOrder decrements stock on the outer client instead of tx, escaping its own transaction; four smaller query bugs also need fixing.",
"verdict": "unsafe",
"finding_responses": [
{
"ref": "F1",
"stance": "confirmed",
"note": "name is not @id/@unique (only id, email are on Customer); changed findUnique to findFirst (line 6)."
},
{
"ref": "F2",
"stance": "confirmed",
"note": "MySQL has no mode option and its default collation is already case-insensitive; removed mode: 'insensitive' (line 11)."
},
{
"ref": "F3",
"stance": "confirmed",
"note": "include only takes relations; Product.sku is a scalar and findMany already returns it, so the include was removed (line 12)."
},
{
"ref": "F4",
"stance": "confirmed",
"note": "orderBy direction must be lower case; changed 'ASC' to 'asc' (line 13)."
},
{
"ref": "F5",
"stance": "confirmed",
"note": "SQL injection via string-interpolated $queryRawUnsafe; replaced with a $queryRaw tagged template so ${email} is bound as a parameter (line 18)."
},
{
"ref": "F6",
"stance": "confirmed",
"note": "the decrement ran on the outer prisma client, not tx, so it committed outside the transaction; changed to tx.product.update (line 25)."
},
{
"ref": "F7",
"stance": "confirmed",
"note": "one product.update per line; each line decrements a different product by a different amount so there is no bulk updateMany equivalent without raw SQL - kept per-line but now inside tx (see F6) so it is atomic with the rest of the order."
},
{
"ref": "F8",
"stance": "confirmed",
"note": "one orderItem.create per line; replaced the loop with a single tx.orderItem.createMany call after the loop (line 26)."
}
],
"extra_findings": [
{
"title": "Order total is hard-coded to 0",
"severity": "warning",
"line": 23,
"evidence": "data: { customerId, total: 0 }",
"problem": "Every Order is persisted with total: 0 regardless of its line items and product prices, so the stored total is always wrong - this matches the note that placeOrder 'sometimes leaves stock wrong', but here it's the total that's silently wrong on every call, not just sometimes.",
"fix": "Compute the total inside the transaction from the ordered products (e.g. tx.product.findMany the line productIds, sum price * quantity) and either pass that into tx.order.create's data or update it with tx.order.update once the amount is known, instead of the literal 0."
},
{
"title": "Stock is decremented with no check that enough stock exists",
"severity": "warning",
"line": 25,
"evidence": "data: { stock: { decrement: line.quantity } }",
"problem": "The update has no condition on the current stock, so decrementing by more than what is in stock still succeeds and Product.stock can go negative (oversell) instead of the order being rejected.",
"fix": "Constrain the update to rows with enough stock, e.g. tx.product.update({ where: { id: line.productId, stock: { gte: line.quantity } }, data: { stock: { decrement: line.quantity } } }), and treat a missing/failed update (record not found) as insufficient stock so the transaction is rolled back."
}
],
"fixed_code": "import { PrismaClient } from '@prisma/client'\n\nconst prisma = new PrismaClient()\n\nexport async function customerByName(name: string) {\n return prisma.customer.findFirst({ where: { name } })\n}\n\nexport async function searchProducts(term: string) {\n return prisma.product.findMany({\n where: { name: { contains: term } },\n orderBy: { price: 'asc' },\n })\n}\n\nexport async function ordersForEmail(email: string) {\n return prisma.$queryRaw`SELECT * FROM \\`Order\\` o JOIN Customer c ON c.id = o.customerId WHERE c.email = ${email}`\n}\n\nexport async function placeOrder(customerId: number, lines: { productId: number; quantity: number }[]) {\n return prisma.$transaction(async (tx) => {\n const order = await tx.order.create({ data: { customerId, total: 0 } })\n for (const line of lines) {\n await tx.product.update({ where: { id: line.productId }, data: { stock: { decrement: line.quantity } } })\n }\n await tx.orderItem.createMany({\n data: lines.map((line) => ({ orderId: order.id, productId: line.productId, quantity: line.quantity })),\n })\n return order\n })\n}\n",
"changes": [
"line 6: findUnique -> findFirst because name is not a unique field (F1).",
"line 11: removed mode: 'insensitive' - unsupported on MySQL (F2).",
"line 12: removed the invalid include: { sku: true } - sku is a scalar and is already returned (F3).",
"line 13: 'ASC' -> 'asc' (F4).",
"line 18: $queryRawUnsafe template string -> $queryRaw tagged template with email bound as a parameter, closing the SQL-injection hole (F5).",
"line 25: prisma.product.update -> tx.product.update so the decrement is part of the transaction (F6).",
"line 26: moved orderItem.create out of the loop into one tx.orderItem.createMany call after the loop (F8); the per-line tx.product.update stays because each line decrements a different product by a different amount, so it remains N+1 (F7)."
],
"to_confirm": [
"order.total is still hard-coded to 0 - should placeOrder compute it from the ordered products' prices, and if so at what price (current Product.price, or a price supplied by the caller)?",
"Should placeOrder check that a product has enough stock before decrementing and roll back the order when it doesn't, or is negative stock acceptable (e.g. a backorder)?",
"If lines contains the same productId twice, should the quantities be merged before creating OrderItem rows? As written, two rows with the same (orderId, productId) would violate the @@id([orderId, productId]) compound key and throw."
]
}Base URL and envelope
Base URL: https://api.skillsafe.ai/. Every call sends
Content-Type: application/json and, once you have a token, Authorization: Bearer YOUR_TOKEN.
The request body for /estimate, /run and /run-stream is the input object
itself — no wrapper.
| Method and path | Body | data |
|---|---|---|
POST /guest | {"slug": "orm-query-check"}, no Authorization | {token, guest_id} |
GET /me | — | {subject_type, subject_id, credits, profile?} |
POST /estimate | the input object | {hold_credits, min_credits, sponsor_enabled, ...} — no charge, no job |
POST /run | the input object; header Idempotency-Key | {job_id, ...} |
GET /jobs/{id} | — | {status, output: {output}, charged_credits, truncated, error} |
POST /run-stream | the input object; header Idempotency-Key | an SSE stream, or a plain envelope on an idempotent replay |
Success is {"data": ...}; failure is {"error": {"code": "...", "message": "...", "details": ...}}.
Errors
These are the only failure paths sdk.js and app.js handle; the SDK names no other codes.
| What | Shape | What to do |
|---|---|---|
| Any non-2xx JSON response | {"error": {"code", "message", "details"}} | The SDK throws an error carrying status, code and details; the message falls back to the HTTP status text. |
401 | error envelope | The page treats it as an expired session: it forgets the user and asks you to sign in again. |
402 | error envelope | Not enough credits for the run: top up. The page prices every run first and never submits into a 402 knowingly. |
SSE event: error | data: {code, message, job_id} | Raised after the stream ends, with code and job_id. The page keeps whatever part of the reply had already arrived. |
Job status: "failed" | GET /jobs/{id} is terminal | job.error says why; there is no reply to parse. |
Idempotency-Key
Send an Idempotency-Key header on /run and /run-stream. The page's key is
orm-query-check:<lane>:, e.g.
orm-query-check:write: for the write example below, where <hash> is kit.js
hashInput(input) (a 53-bit hash of the key-sorted input, in base 36) and <attempt> starts at 1.
A network retry with the same key never bills twice; the same schema in the two lanes is two runs; the one
automatic reformat retry (with retry_note) is attempt 2. Any stable hash of your exact input works; the
samples below use the first 16 hex digits of its SHA-256. Re-sending a used key replays that job: /run-stream
then answers with a plain JSON envelope instead of a stream, and the samples handle both.
Estimate, hold and charge
POST /estimatereturns the worst case for this input without charging or creating a job. It validates nothing about the body (the page guards that the body is a JSON object before sending), so a clean estimate does not mean the run input is right.hold_creditsis what a run reserves from your balance while it works. It is not the price.min_credits, when present, is the balance below which the run cannot start; with a balance underhold_creditsthe run can still start but the reply may be cut short.charged_creditson the finished job (or the stream's final payload) is what you pay — usually far less than the hold.truncated: truemeans the reply stopped early because the balance ran out. The page repairs the cut JSON withrecon.jscloseJson, shows the sections that arrived, and suggests topping up.- Metered runs need a personal, signed-in token; a guest token is enough for
/meand/estimate. The page lets a guest press Run only when the estimate reportssponsor_enabled: true.
Step by step
1. A tiny client helper
It sends JSON, adds the bearer token, returns data and turns the error envelope into an exception.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"
# api METHOD PATH [JSON_BODY] [extra curl args...]
# Prints the envelope: {"data": ...} on 2xx, {"error": {"code", "message", "details"}} otherwise.
api() {
local method="$1" path="$2" body="${3-}"
shift 2
[ $# -gt 0 ] && shift
if [ -n "$body" ]; then
curl -sS -X "$method" "$BASE$path" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-binary "$body" "$@"
else
curl -sS -X "$method" "$BASE$path" \
-H "Authorization: Bearer $TOKEN" "$@"
fi
}import json
import requests
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"
class ApiError(Exception):
def __init__(self, status, code, message, details):
super().__init__(f"{status} {code}: {message}")
self.status, self.code, self.details = status, code, details
def call(method, path, body=None, headers=None, auth=True):
"""Send body as JSON; return the envelope's "data", raise ApiError on non-2xx."""
h = {"Content-Type": "application/json"}
if auth and TOKEN:
h["Authorization"] = "Bearer " + TOKEN
h.update(headers or {})
r = requests.request(method, BASE + path, headers=h,
data=None if body is None else json.dumps(body), timeout=300)
try:
env = r.json()
except ValueError:
env = {}
if not r.ok:
err = env.get("error") or {}
raise ApiError(r.status_code, err.get("code"), err.get("message") or r.reason, err.get("details"))
return env.get("data")// Node 20+ (run as an ES module, e.g. api.mjs) or a browser console.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
let token = TOKEN;
async function call(method, path, body, extraHeaders = {}, auth = true) {
const headers = { "Content-Type": "application/json", ...extraHeaders };
if (auth && token) headers["Authorization"] = "Bearer " + token;
const res = await fetch(BASE + path, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const env = await res.json().catch(() => ({}));
if (!res.ok) {
const err = new Error((env.error && env.error.message) || res.statusText);
err.status = res.status;
err.code = env.error && env.error.code;
err.details = env.error && env.error.details;
throw err;
}
return env.data;
}// main.go - the imports cover every step on this page; steps 2-4 go in main,
// steps 5 and 6 add package-level functions.
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = os.Getenv("SKILLSAFE_TOKEN") // or "YOUR_TOKEN"
var client = &http.Client{Timeout: 5 * time.Minute}
type APIError struct {
Status int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
Details json.RawMessage `json:"details"`
}
func (e *APIError) Error() string { return fmt.Sprintf("%d %s: %s", e.Status, e.Code, e.Message) }
// call sends body as JSON and decodes the envelope's "data" into out.
func call(method, path string, body any, headers map[string]string, out any) error {
var rdr io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return err
}
rdr = bytes.NewReader(b)
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var env struct {
Data json.RawMessage `json:"data"`
Error *APIError `json:"error"`
}
_ = json.NewDecoder(resp.Body).Decode(&env)
if resp.StatusCode < 200 || resp.StatusCode > 299 {
e := env.Error
if e == nil {
e = &APIError{Message: resp.Status}
}
e.Status = resp.StatusCode
return e
}
if out != nil && len(env.Data) > 0 {
return json.Unmarshal(env.Data, out)
}
return nil
}
func main() {
// steps 2-4 go here
}// Api.java - Java 17+, java.net.http plus Jackson (com.fasterxml.jackson.core:jackson-databind).
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.HexFormat;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Api {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN";
static final ObjectMapper JSON = new ObjectMapper();
static final HttpClient HTTP = HttpClient.newHttpClient();
static class ApiException extends RuntimeException {
final int status;
final String code;
final JsonNode details;
ApiException(int status, String code, String message, JsonNode details) {
super(status + " " + code + ": " + message);
this.status = status;
this.code = code;
this.details = details;
}
}
static HttpRequest.Builder request(String method, String path, Object body, Map<String, String> headers) throws Exception {
HttpRequest.BodyPublisher pub = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body));
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.timeout(Duration.ofMinutes(5))
.header("Content-Type", "application/json")
.method(method, pub);
if (token != null && !token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
return b;
}
static ApiException apiError(int status, JsonNode env) {
JsonNode err = env == null ? JSON.createObjectNode() : env.path("error");
return new ApiException(status, err.path("code").asText(null),
err.path("message").asText("HTTP " + status), err.get("details"));
}
/** Send body as JSON; return the envelope's "data", throw ApiException on non-2xx. */
static JsonNode call(String method, String path, Object body, Map<String, String> headers) throws Exception {
HttpResponse<String> res = HTTP.send(request(method, path, body, headers).build(),
HttpResponse.BodyHandlers.ofString());
JsonNode env;
try { env = JSON.readTree(res.body()); } catch (Exception e) { env = null; }
if (res.statusCode() < 200 || res.statusCode() > 299) throw apiError(res.statusCode(), env);
return env == null ? JSON.missingNode() : env.path("data");
}
public static void main(String[] args) throws Exception {
// the statements of steps 2-6 go here; steps 5 and 6 also add static methods to this class
}
}require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"
$token = TOKEN
class ApiError < StandardError
attr_reader :status, :code, :details
def initialize(status, code, message, details)
super("#{status} #{code}: #{message}")
@status, @code, @details = status, code, details
end
end
def build_request(method, path, body = nil, headers = {}, auth = true)
uri = URI(BASE + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{$token}" if auth && !$token.to_s.empty?
headers.each { |k, v| req[k] = v }
req.body = JSON.generate(body) unless body.nil?
[uri, req]
end
# Send body as JSON; return the envelope's "data", raise ApiError on non-2xx.
def call(method, path, body = nil, headers = {}, auth = true)
uri, req = build_request(method, path, body, headers, auth)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 300) { |h| h.request(req) }
env = (JSON.parse(res.body.to_s) rescue {})
unless res.is_a?(Net::HTTPSuccess)
err = env["error"] || {}
raise ApiError.new(res.code.to_i, err["code"], err["message"] || res.message, err["details"])
end
env["data"]
end<?php
// PHP 8.1+ with the curl extension.
const BASE = "https://api.skillsafe.ai/v1/app-api";
$token = "YOUR_TOKEN";
class ApiError extends Exception {
public function __construct(public int $status, public ?string $errCode, string $message, public mixed $details) {
parent::__construct("$status $errCode: $message");
}
}
function apiError(int $status, array $env): ApiError {
$e = $env["error"] ?? [];
return new ApiError($status, $e["code"] ?? null, $e["message"] ?? "HTTP $status", $e["details"] ?? null);
}
/** Send $body as JSON; return the envelope's "data", throw ApiError on non-2xx. */
function call(string $method, string $path, mixed $body = null, array $headers = [], bool $auth = true): mixed {
global $token;
$h = ["Content-Type: application/json"];
if ($auth && $token !== "") $h[] = "Authorization: Bearer $token";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $h,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 300,
]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_SLASHES));
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$env = json_decode($raw, true) ?: [];
if ($status < 200 || $status > 299) throw apiError($status, $env);
return $env["data"] ?? null;
}// Program.cs - .NET 8, top-level statements. Steps 2-6 continue below the helper;
// keep the ApiException class at the very end of the file.
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
const string Base = "https://api.skillsafe.ai/v1/app-api";
var token = "YOUR_TOKEN";
var http = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
HttpRequestMessage Build(HttpMethod method, string path, JsonNode? body,
Dictionary<string, string>? headers = null, bool auth = true)
{
var req = new HttpRequestMessage(method, Base + path);
if (auth && token != "") req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
if (headers != null) foreach (var (k, v) in headers) req.Headers.TryAddWithoutValidation(k, v);
if (body != null) req.Content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json");
return req;
}
ApiException ApiErr(int status, JsonNode? env, string? reason) =>
new(status, env?["error"]?["code"]?.GetValue<string>(),
env?["error"]?["message"]?.GetValue<string>() ?? reason ?? "", env?["error"]?["details"]);
// Send body as JSON; return the envelope's "data", throw ApiException on non-2xx.
async Task<JsonNode?> Call(HttpMethod method, string path, JsonNode? body = null,
Dictionary<string, string>? headers = null, bool auth = true)
{
using var res = await http.SendAsync(Build(method, path, body, headers, auth));
var text = await res.Content.ReadAsStringAsync();
JsonNode? env = null;
try { env = JsonNode.Parse(text); } catch (JsonException) { }
if (!res.IsSuccessStatusCode) throw ApiErr((int)res.StatusCode, env, res.ReasonPhrase);
return env?["data"];
}
// ... steps 2-6 go here ...
class ApiException(int status, string? code, string message, JsonNode? details)
: Exception($"{status} {code}: {message}")
{
public int Status => status;
public string? Code => code;
public JsonNode? Details => details;
}2. Get a token
A guest token comes from POST /guest with {"slug": "orm-query-check"} and returns
token and guest_id. It can read /me and price runs. Metered runs need a
personal, signed-in token: copy yours from the token page.
# A guest token: no Authorization header, body {"slug": "orm-query-check"}.
GUEST=$(curl -sS -X POST "$BASE/guest" \
-H "Content-Type: application/json" \
--data-binary '{"slug":"orm-query-check"}')
echo "$GUEST" | jq '.data.guest_id'
TOKEN=$(echo "$GUEST" | jq -r '.data.token')
# Metered runs (task "write" and "review") need a personal, signed-in token instead:
# copy it from https://orm-query-check.skillsafe.ai/tokens.html
# TOKEN="YOUR_TOKEN"# A guest token: no Authorization header, body {"slug": "orm-query-check"}.
guest = call("POST", "/guest", {"slug": "orm-query-check"}, auth=False)
print("guest_id:", guest["guest_id"])
TOKEN = guest["token"]
# Metered runs (task "write" and "review") need a personal, signed-in token instead:
# copy it from https://orm-query-check.skillsafe.ai/tokens.html
# TOKEN = "YOUR_TOKEN"// A guest token: no Authorization header, body {"slug": "orm-query-check"}.
const guest = await call("POST", "/guest", { slug: "orm-query-check" }, {}, false);
console.log("guest_id:", guest.guest_id);
token = guest.token;
// Metered runs (task "write" and "review") need a personal, signed-in token instead:
// copy it from https://orm-query-check.skillsafe.ai/tokens.html
// token = TOKEN;// A guest token: no Authorization header, body {"slug": "orm-query-check"}.
var guest struct {
Token string `json:"token"`
GuestID string `json:"guest_id"`
}
personal := token
token = "" // call() sends no Authorization header while token is empty
if err := call("POST", "/guest", map[string]string{"slug": "orm-query-check"}, nil, &guest); err != nil {
panic(err)
}
fmt.Println("guest_id:", guest.GuestID)
token = guest.Token
// Metered runs (task "write" and "review") need a personal, signed-in token instead:
// copy it from https://orm-query-check.skillsafe.ai/tokens.html into SKILLSAFE_TOKEN.
if personal != "" {
token = personal
}// A guest token: no Authorization header, body {"slug": "orm-query-check"}.
String personal = token;
token = null; // request() sends no Authorization header while token is null
JsonNode guest = call("POST", "/guest", Map.of("slug", "orm-query-check"), null);
System.out.println("guest_id: " + guest.path("guest_id").asText());
token = guest.path("token").asText();
// Metered runs (task "write" and "review") need a personal, signed-in token instead:
// copy it from https://orm-query-check.skillsafe.ai/tokens.html
// token = personal;# A guest token: no Authorization header, body {"slug": "orm-query-check"}.
guest = call("POST", "/guest", { slug: "orm-query-check" }, {}, false)
puts "guest_id: #{guest["guest_id"]}"
$token = guest["token"]
# Metered runs (task "write" and "review") need a personal, signed-in token instead:
# copy it from https://orm-query-check.skillsafe.ai/tokens.html
# $token = TOKEN// A guest token: no Authorization header, body {"slug": "orm-query-check"}.
$guest = call("POST", "/guest", ["slug" => "orm-query-check"], [], false);
echo "guest_id: {$guest["guest_id"]}\n";
$token = $guest["token"];
// Metered runs (task "write" and "review") need a personal, signed-in token instead:
// copy it from https://orm-query-check.skillsafe.ai/tokens.html
// $token = "YOUR_TOKEN";// A guest token: no Authorization header, body {"slug": "orm-query-check"}.
var guest = (await Call(HttpMethod.Post, "/guest",
new JsonObject { ["slug"] = "orm-query-check" }, auth: false))!;
Console.WriteLine($"guest_id: {guest["guest_id"]}");
token = guest["token"]!.GetValue<string>();
// Metered runs (task "write" and "review") need a personal, signed-in token instead:
// copy it from https://orm-query-check.skillsafe.ai/tokens.html
// token = "YOUR_TOKEN";3. Who am I: GET /me
api GET /me | jq '.data'
# {"subject_type": "...", "subject_id": "...", "credits": 12345, ...}me = call("GET", "/me")
print(me["subject_type"], me["subject_id"], "credits:", me.get("credits"))const me = await call("GET", "/me");
console.log(me.subject_type, me.subject_id, "credits:", me.credits);var me map[string]any
if err := call("GET", "/me", nil, nil, &me); err != nil {
panic(err)
}
fmt.Println(me["subject_type"], me["subject_id"], "credits:", me["credits"])JsonNode me = call("GET", "/me", null, null);
System.out.println(me.path("subject_type").asText() + " " + me.path("subject_id").asText()
+ " credits: " + me.path("credits"));me = call("GET", "/me")
puts "#{me["subject_type"]} #{me["subject_id"]} credits: #{me["credits"]}"$me = call("GET", "/me");
echo "{$me["subject_type"]} {$me["subject_id"]} credits: " . ($me["credits"] ?? "?") . "\n";var me = (await Call(HttpMethod.Get, "/me"))!;
Console.WriteLine($"{me["subject_type"]} {me["subject_id"]} credits: {me["credits"]}");4. Price a write run: POST /estimate
The body is the input object. facts here is a hand-written subset of what the page sends (the keys from the
facts table); run pqscan.js to produce the full object.
cat > schema.prisma <<'PRISMA'
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
publishedAt DateTime?
author User @relation(fields: [authorId], references: [id])
authorId Int
}
PRISMA
# facts is sent as a STRING holding JSON; the page builds it with kit.js facts().
cat > facts.json <<'JSON'
{
"provider": "postgresql",
"generator": "prisma-client",
"client_api": "prisma-orm-7 (prisma-client generator)",
"capabilities": {"provider": "postgresql", "mode_insensitive": true, "and_return": true, "skip_duplicates": true, "json_filters": "array path like ['a','b']", "scalar_lists": true, "is_set": false},
"models": [
{"model": "User", "accessor": "user", "fields": ["id:Int", "email:String", "name:String?", "posts:Post[](relation, list)"], "unique": ["id", "email"]},
{"model": "Post", "accessor": "post", "fields": ["id:Int", "title:String", "published:Boolean", "publishedAt:DateTime?", "author:User(relation)", "authorId:Int"], "unique": ["id"]}
],
"enums": {},
"schema_problems": []
}
JSON
INPUT=$(jq -n \
--rawfile schema schema.prisma \
--arg facts "$(jq -c . facts.json)" \
--arg request "Published posts whose title contains a search term, ignoring case, newest first by publish date, 20 per page, with the author's name." \
'{task: "write", schema: $schema, request: $request, facts: $facts}')
api POST /estimate "$INPUT" | jq '.data'
# hold_credits = the reservation, not the price; min_credits = the balance floorSCHEMA = """generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
publishedAt DateTime?
author User @relation(fields: [authorId], references: [id])
authorId Int
}
"""
# facts is sent as a STRING holding JSON; the page builds it with kit.js facts().
FACTS = """{
"provider": "postgresql",
"generator": "prisma-client",
"client_api": "prisma-orm-7 (prisma-client generator)",
"capabilities": {"provider": "postgresql", "mode_insensitive": true, "and_return": true, "skip_duplicates": true, "json_filters": "array path like ['a','b']", "scalar_lists": true, "is_set": false},
"models": [
{"model": "User", "accessor": "user", "fields": ["id:Int", "email:String", "name:String?", "posts:Post[](relation, list)"], "unique": ["id", "email"]},
{"model": "Post", "accessor": "post", "fields": ["id:Int", "title:String", "published:Boolean", "publishedAt:DateTime?", "author:User(relation)", "authorId:Int"], "unique": ["id"]}
],
"enums": {},
"schema_problems": []
}"""
write_input = {
"task": "write",
"schema": SCHEMA,
"request": "Published posts whose title contains a search term, ignoring case, newest first by publish date, 20 per page, with the author's name.",
"facts": FACTS,
}
est = call("POST", "/estimate", write_input) # the body IS the input object
print("hold_credits:", est.get("hold_credits"), "min_credits:", est.get("min_credits"))const SCHEMA = `generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
publishedAt DateTime?
author User @relation(fields: [authorId], references: [id])
authorId Int
}
`;
// facts is sent as a STRING holding JSON; the page builds it with kit.js facts().
const FACTS = `{
"provider": "postgresql",
"generator": "prisma-client",
"client_api": "prisma-orm-7 (prisma-client generator)",
"capabilities": {"provider": "postgresql", "mode_insensitive": true, "and_return": true, "skip_duplicates": true, "json_filters": "array path like ['a','b']", "scalar_lists": true, "is_set": false},
"models": [
{"model": "User", "accessor": "user", "fields": ["id:Int", "email:String", "name:String?", "posts:Post[](relation, list)"], "unique": ["id", "email"]},
{"model": "Post", "accessor": "post", "fields": ["id:Int", "title:String", "published:Boolean", "publishedAt:DateTime?", "author:User(relation)", "authorId:Int"], "unique": ["id"]}
],
"enums": {},
"schema_problems": []
}`;
const writeInput = {
task: "write",
schema: SCHEMA,
request: "Published posts whose title contains a search term, ignoring case, newest first by publish date, 20 per page, with the author's name.",
facts: FACTS,
};
const est = await call("POST", "/estimate", writeInput); // the body IS the input object
console.log("hold_credits:", est.hold_credits, "min_credits:", est.min_credits);schema := `generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
publishedAt DateTime?
author User @relation(fields: [authorId], references: [id])
authorId Int
}
`
// facts is sent as a STRING holding JSON; the page builds it with kit.js facts().
facts := `{
"provider": "postgresql",
"generator": "prisma-client",
"client_api": "prisma-orm-7 (prisma-client generator)",
"capabilities": {"provider": "postgresql", "mode_insensitive": true, "and_return": true, "skip_duplicates": true, "json_filters": "array path like ['a','b']", "scalar_lists": true, "is_set": false},
"models": [
{"model": "User", "accessor": "user", "fields": ["id:Int", "email:String", "name:String?", "posts:Post[](relation, list)"], "unique": ["id", "email"]},
{"model": "Post", "accessor": "post", "fields": ["id:Int", "title:String", "published:Boolean", "publishedAt:DateTime?", "author:User(relation)", "authorId:Int"], "unique": ["id"]}
],
"enums": {},
"schema_problems": []
}`
input := map[string]any{
"task": "write",
"schema": schema,
"request": "Published posts whose title contains a search term, ignoring case, newest first by publish date, 20 per page, with the author's name.",
"facts": facts,
}
var est map[string]any
if err := call("POST", "/estimate", input, nil, &est); err != nil { // the body IS the input object
panic(err)
}
fmt.Println("hold_credits:", est["hold_credits"], "min_credits:", est["min_credits"])String schema = """
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
publishedAt DateTime?
author User @relation(fields: [authorId], references: [id])
authorId Int
}
""";
// facts is sent as a STRING holding JSON; the page builds it with kit.js facts().
String facts = """
{
"provider": "postgresql",
"generator": "prisma-client",
"client_api": "prisma-orm-7 (prisma-client generator)",
"capabilities": {"provider": "postgresql", "mode_insensitive": true, "and_return": true, "skip_duplicates": true, "json_filters": "array path like ['a','b']", "scalar_lists": true, "is_set": false},
"models": [
{"model": "User", "accessor": "user", "fields": ["id:Int", "email:String", "name:String?", "posts:Post[](relation, list)"], "unique": ["id", "email"]},
{"model": "Post", "accessor": "post", "fields": ["id:Int", "title:String", "published:Boolean", "publishedAt:DateTime?", "author:User(relation)", "authorId:Int"], "unique": ["id"]}
],
"enums": {},
"schema_problems": []
}""";
// A LinkedHashMap keeps the key order (and so the step 5 hash) stable across runs.
Map<String, Object> input = new LinkedHashMap<>();
input.put("task", "write");
input.put("schema", schema);
input.put("request", "Published posts whose title contains a search term, ignoring case, newest first by publish date, 20 per page, with the author's name.");
input.put("facts", facts);
JsonNode est = call("POST", "/estimate", input, null); // the body IS the input object
System.out.println("hold_credits: " + est.path("hold_credits") + " min_credits: " + est.path("min_credits"));SCHEMA = <<~'PRISMA'
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
publishedAt DateTime?
author User @relation(fields: [authorId], references: [id])
authorId Int
}
PRISMA
# facts is sent as a STRING holding JSON; the page builds it with kit.js facts().
FACTS = <<~'JSON'.strip
{
"provider": "postgresql",
"generator": "prisma-client",
"client_api": "prisma-orm-7 (prisma-client generator)",
"capabilities": {"provider": "postgresql", "mode_insensitive": true, "and_return": true, "skip_duplicates": true, "json_filters": "array path like ['a','b']", "scalar_lists": true, "is_set": false},
"models": [
{"model": "User", "accessor": "user", "fields": ["id:Int", "email:String", "name:String?", "posts:Post[](relation, list)"], "unique": ["id", "email"]},
{"model": "Post", "accessor": "post", "fields": ["id:Int", "title:String", "published:Boolean", "publishedAt:DateTime?", "author:User(relation)", "authorId:Int"], "unique": ["id"]}
],
"enums": {},
"schema_problems": []
}
JSON
write_input = {
task: "write",
schema: SCHEMA,
request: "Published posts whose title contains a search term, ignoring case, newest first by publish date, 20 per page, with the author's name.",
facts: FACTS
}
est = call("POST", "/estimate", write_input) # the body IS the input object
puts "hold_credits: #{est["hold_credits"]} min_credits: #{est["min_credits"]}"$schema = <<<'PRISMA'
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
publishedAt DateTime?
author User @relation(fields: [authorId], references: [id])
authorId Int
}
PRISMA;
// facts is sent as a STRING holding JSON; the page builds it with kit.js facts().
$facts = <<<'JSON'
{
"provider": "postgresql",
"generator": "prisma-client",
"client_api": "prisma-orm-7 (prisma-client generator)",
"capabilities": {"provider": "postgresql", "mode_insensitive": true, "and_return": true, "skip_duplicates": true, "json_filters": "array path like ['a','b']", "scalar_lists": true, "is_set": false},
"models": [
{"model": "User", "accessor": "user", "fields": ["id:Int", "email:String", "name:String?", "posts:Post[](relation, list)"], "unique": ["id", "email"]},
{"model": "Post", "accessor": "post", "fields": ["id:Int", "title:String", "published:Boolean", "publishedAt:DateTime?", "author:User(relation)", "authorId:Int"], "unique": ["id"]}
],
"enums": {},
"schema_problems": []
}
JSON;
$writeInput = [
"task" => "write",
"schema" => $schema,
"request" => "Published posts whose title contains a search term, ignoring case, newest first by publish date, 20 per page, with the author's name.",
"facts" => $facts,
];
$est = call("POST", "/estimate", $writeInput); // the body IS the input object
echo "hold_credits: " . ($est["hold_credits"] ?? "?") . " min_credits: " . ($est["min_credits"] ?? "?") . "\n";var schema = """
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
publishedAt DateTime?
author User @relation(fields: [authorId], references: [id])
authorId Int
}
""";
// facts is sent as a STRING holding JSON; the page builds it with kit.js facts().
var facts = """
{
"provider": "postgresql",
"generator": "prisma-client",
"client_api": "prisma-orm-7 (prisma-client generator)",
"capabilities": {"provider": "postgresql", "mode_insensitive": true, "and_return": true, "skip_duplicates": true, "json_filters": "array path like ['a','b']", "scalar_lists": true, "is_set": false},
"models": [
{"model": "User", "accessor": "user", "fields": ["id:Int", "email:String", "name:String?", "posts:Post[](relation, list)"], "unique": ["id", "email"]},
{"model": "Post", "accessor": "post", "fields": ["id:Int", "title:String", "published:Boolean", "publishedAt:DateTime?", "author:User(relation)", "authorId:Int"], "unique": ["id"]}
],
"enums": {},
"schema_problems": []
}
""";
var input = new JsonObject
{
["task"] = "write",
["schema"] = schema,
["request"] = "Published posts whose title contains a search term, ignoring case, newest first by publish date, 20 per page, with the author's name.",
["facts"] = facts,
};
var est = (await Call(HttpMethod.Post, "/estimate", input))!; // the body IS the input object
Console.WriteLine($"hold_credits: {est["hold_credits"]} min_credits: {est["min_credits"]}");5. Run it: POST /run, then poll GET /jobs/{id}
Send the Idempotency-Key, poll about once a second until status is succeeded or
failed, then read the reply text at job.output.output and parse it as JSON.
# orm-query-check:<lane>:<hash of the exact input>:a<attempt>
KEY="orm-query-check:write:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
RUN=$(api POST /run "$INPUT" -H "Idempotency-Key: $KEY")
JOB_ID=$(echo "$RUN" | jq -r '.data.job_id // empty')
[ -n "$JOB_ID" ] || { echo "$RUN" | jq '.error'; exit 1; } # 401: sign in again, 402: top up
while :; do
JOB=$(api GET "/jobs/$JOB_ID")
STATUS=$(echo "$JOB" | jq -r '.data.status')
case "$STATUS" in succeeded|failed) break ;; esac
sleep 1
done
echo "$JOB" | jq '.data | {status, charged_credits, truncated, error}'
# The model's reply is a JSON object serialised as text in job.output.output:
echo "$JOB" | jq -r '.data.output.output' | jq '{headline, method, model, code}'import hashlib
import time
from urllib.parse import quote
def idem_key(lane, inp, attempt=1):
"""The app's format: orm-query-check:<lane>:<hash of the exact input>:a<attempt>."""
digest = hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16]
return f"orm-query-check:{lane}:{digest}:a{attempt}"
def wait_for_job(job_id, interval=1.0, timeout=180):
start = time.time()
while True:
job = call("GET", "/jobs/" + quote(job_id, safe=""))
if job["status"] in ("succeeded", "failed"):
return job
if time.time() - start > timeout:
raise TimeoutError("job timed out")
time.sleep(interval)
try:
started = call("POST", "/run", write_input,
headers={"Idempotency-Key": idem_key("write", write_input)})
except ApiError as e:
if e.status == 402:
raise SystemExit("Not enough credits for this run - top up.")
if e.status == 401:
raise SystemExit("The token expired or is not signed in - get a new one.")
raise
job = wait_for_job(started["job_id"])
if job["status"] == "failed":
raise RuntimeError(f"job failed: {job.get('error')}")
reply = json.loads(job["output"]["output"]) # the reply text is one JSON object
print(reply["headline"])
print(reply["code"])
print("charged_credits:", job.get("charged_credits"), "truncated:", job.get("truncated") is True)// The app's format: orm-query-check:<lane>:<hash of the exact input>:a<attempt>.
async function idemKey(lane, input, attempt = 1) {
const bytes = new TextEncoder().encode(JSON.stringify(input));
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
const hex = Array.from(digest.slice(0, 8), (b) => b.toString(16).padStart(2, "0")).join("");
return `orm-query-check:${lane}:${hex}:a${attempt}`;
}
async function waitForJob(jobId, intervalMs = 1000, timeoutMs = 180000) {
const start = Date.now();
for (;;) {
const job = await call("GET", "/jobs/" + encodeURIComponent(jobId));
if (job.status === "succeeded" || job.status === "failed") return job;
if (Date.now() - start > timeoutMs) throw new Error("job timed out");
await new Promise((r) => setTimeout(r, intervalMs));
}
}
let started;
try {
started = await call("POST", "/run", writeInput, { "Idempotency-Key": await idemKey("write", writeInput) });
} catch (err) {
if (err.status === 402) throw new Error("Not enough credits for this run - top up.");
if (err.status === 401) throw new Error("The token expired or is not signed in - get a new one.");
throw err;
}
const job = await waitForJob(started.job_id);
if (job.status === "failed") throw new Error("job failed: " + JSON.stringify(job.error));
const reply = JSON.parse(job.output.output); // the reply text is one JSON object
console.log(reply.headline);
console.log(reply.code);
console.log("charged_credits:", job.charged_credits, "truncated:", job.truncated === true);// --- package level ---
// idemKey is the app's format: orm-query-check:<lane>:<hash of the exact input>:a<attempt>.
func idemKey(lane string, input map[string]any, attempt int) string {
b, _ := json.Marshal(input) // map keys marshal sorted, so the hash is stable
sum := sha256.Sum256(b)
return fmt.Sprintf("orm-query-check:%s:%x:a%d", lane, sum[:8], attempt)
}
func waitForJob(jobID string) (map[string]any, error) {
deadline := time.Now().Add(3 * time.Minute)
for {
var job map[string]any
if err := call("GET", "/jobs/"+url.PathEscape(jobID), nil, nil, &job); err != nil {
return nil, err
}
if s, _ := job["status"].(string); s == "succeeded" || s == "failed" {
return job, nil
}
if time.Now().After(deadline) {
return nil, fmt.Errorf("job %s timed out", jobID)
}
time.Sleep(time.Second)
}
}
// replyOf reads the reply text at job.output.output and parses the JSON object in it.
func replyOf(job map[string]any) (map[string]any, error) {
if s, _ := job["status"].(string); s == "failed" {
return nil, fmt.Errorf("job failed: %v", job["error"])
}
out, _ := job["output"].(map[string]any)
text, _ := out["output"].(string)
var reply map[string]any
err := json.Unmarshal([]byte(text), &reply)
return reply, err
}
// --- in main ---
var started struct {
JobID string `json:"job_id"`
}
if err := call("POST", "/run", input, map[string]string{"Idempotency-Key": idemKey("write", input, 1)}, &started); err != nil {
panic(err) // *APIError: Status 401 = sign in again, 402 = not enough credits
}
job, err := waitForJob(started.JobID)
if err != nil {
panic(err)
}
reply, err := replyOf(job)
if err != nil {
panic(err)
}
fmt.Println(reply["headline"])
fmt.Println(reply["code"])
fmt.Println("charged_credits:", job["charged_credits"], "truncated:", job["truncated"] == true)// --- static methods in class Api ---
/** The app's format: orm-query-check:<lane>:<hash of the exact input>:a<attempt>. */
static String idemKey(String lane, Object input, int attempt) throws Exception {
byte[] d = MessageDigest.getInstance("SHA-256").digest(JSON.writeValueAsBytes(input));
return "orm-query-check:" + lane + ":" + HexFormat.of().formatHex(d, 0, 8) + ":a" + attempt;
}
static JsonNode waitForJob(String jobId) throws Exception {
long deadline = System.currentTimeMillis() + 180_000;
while (true) {
JsonNode job = call("GET", "/jobs/" + URLEncoder.encode(jobId, StandardCharsets.UTF_8), null, null);
String status = job.path("status").asText();
if (status.equals("succeeded") || status.equals("failed")) return job;
if (System.currentTimeMillis() > deadline) throw new IllegalStateException("job timed out");
Thread.sleep(1000);
}
}
// --- in main ---
JsonNode started;
try {
started = call("POST", "/run", input, Map.of("Idempotency-Key", idemKey("write", input, 1)));
} catch (ApiException e) {
if (e.status == 402) throw new IllegalStateException("Not enough credits for this run - top up.", e);
if (e.status == 401) throw new IllegalStateException("The token expired or is not signed in.", e);
throw e;
}
JsonNode job = waitForJob(started.path("job_id").asText());
if (job.path("status").asText().equals("failed")) {
throw new IllegalStateException("job failed: " + job.path("error"));
}
JsonNode reply = JSON.readTree(job.path("output").path("output").asText()); // one JSON object
System.out.println(reply.path("headline").asText());
System.out.println(reply.path("code").asText());
System.out.println("charged_credits: " + job.path("charged_credits")
+ " truncated: " + job.path("truncated").asBoolean(false));require "digest"
# The app's format: orm-query-check:<lane>:<hash of the exact input>:a<attempt>.
def idem_key(lane, input, attempt = 1)
"orm-query-check:#{lane}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a#{attempt}"
end
def wait_for_job(job_id, interval: 1, timeout: 180)
start = Time.now
loop do
job = call("GET", "/jobs/#{URI.encode_www_form_component(job_id)}")
return job if %w[succeeded failed].include?(job["status"])
raise "job timed out" if Time.now - start > timeout
sleep interval
end
end
begin
started = call("POST", "/run", write_input, { "Idempotency-Key" => idem_key("write", write_input) })
rescue ApiError => e
abort "Not enough credits for this run - top up." if e.status == 402
abort "The token expired or is not signed in - get a new one." if e.status == 401
raise
end
job = wait_for_job(started["job_id"])
raise "job failed: #{job["error"]}" if job["status"] == "failed"
reply = JSON.parse(job["output"]["output"]) # the reply text is one JSON object
puts reply["headline"], reply["code"]
puts "charged_credits: #{job["charged_credits"]} truncated: #{job["truncated"] == true}"// The app's format: orm-query-check:<lane>:<hash of the exact input>:a<attempt>.
function idemKey(string $lane, array $input, int $attempt = 1): string {
$hash = substr(hash("sha256", json_encode($input, JSON_UNESCAPED_SLASHES)), 0, 16);
return "orm-query-check:$lane:$hash:a$attempt";
}
function waitForJob(string $jobId, int $timeout = 180): array {
$start = time();
while (true) {
$job = call("GET", "/jobs/" . rawurlencode($jobId));
if (in_array($job["status"] ?? "", ["succeeded", "failed"], true)) return $job;
if (time() - $start > $timeout) throw new RuntimeException("job timed out");
sleep(1);
}
}
try {
$started = call("POST", "/run", $writeInput, ["Idempotency-Key" => idemKey("write", $writeInput)]);
} catch (ApiError $e) {
if ($e->status === 402) exit("Not enough credits for this run - top up.\n");
if ($e->status === 401) exit("The token expired or is not signed in - get a new one.\n");
throw $e;
}
$job = waitForJob($started["job_id"]);
if ($job["status"] === "failed") throw new RuntimeException("job failed: " . json_encode($job["error"] ?? null));
$reply = json_decode($job["output"]["output"], true, 512, JSON_THROW_ON_ERROR); // one JSON object
echo $reply["headline"], "\n", $reply["code"], "\n";
echo "charged_credits: " . ($job["charged_credits"] ?? "?") . " truncated: " . (($job["truncated"] ?? false) === true ? "yes" : "no") . "\n";// The app's format: orm-query-check:<lane>:<hash of the exact input>:a<attempt>.
string IdemKey(string lane, JsonNode input, int attempt = 1)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input.ToJsonString()));
return $"orm-query-check:{lane}:{Convert.ToHexString(hash, 0, 8).ToLowerInvariant()}:a{attempt}";
}
async Task<JsonNode> WaitForJob(string jobId)
{
var deadline = DateTime.UtcNow.AddMinutes(3);
while (true)
{
var j = (await Call(HttpMethod.Get, "/jobs/" + Uri.EscapeDataString(jobId)))!;
if (j["status"]?.GetValue<string>() is "succeeded" or "failed") return j;
if (DateTime.UtcNow > deadline) throw new TimeoutException("job timed out");
await Task.Delay(1000);
}
}
JsonNode started;
try
{
started = (await Call(HttpMethod.Post, "/run", input,
new Dictionary<string, string> { ["Idempotency-Key"] = IdemKey("write", input) }))!;
}
catch (ApiException e) when (e.Status is 401 or 402)
{
throw new Exception(e.Status == 402 ? "Not enough credits for this run - top up."
: "The token expired or is not signed in - get a new one.", e);
}
var job = await WaitForJob(started["job_id"]!.GetValue<string>());
if (job["status"]?.GetValue<string>() == "failed") throw new Exception($"job failed: {job["error"]?.ToJsonString()}");
var reply = JsonNode.Parse(job["output"]!["output"]!.GetValue<string>())!; // one JSON object
Console.WriteLine(reply["headline"]);
Console.WriteLine(reply["code"]);
Console.WriteLine($"charged_credits: {job["charged_credits"]} truncated: {job["truncated"]}");6. Stream it: POST /run-stream
The response is Server-Sent Events: delta ({text}, a piece of the reply), job,
done or pending (the final payload {job_id, status, charged_credits, output, truncated})
and error ({code, message, job_id}). Browsers may receive only heartbeats and a final
done, with no deltas, so always read the reply from the final payload's output.output and use the
joined deltas only as a fallback. The samples treat a final payload that is not yet succeeded or
failed as unfinished and poll /jobs/{job_id}.
KEY="orm-query-check:write:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
# -N turns off buffering. A replay of a key already used answers with a plain {"data": ...} JSON body.
curl -sS -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
--data-binary "$INPUT" |
while IFS= read -r line; do
case "$line" in
"{"*) echo "$line" | jq -r '.data.output.output // .error' ;; # plain JSON: replay or error
"event:"*) EVENT="${line#event:}"; EVENT="${EVENT# }" ;;
"data:"*) DATA="${DATA-}${line#data:}" ;;
"")
if [ -n "${DATA-}" ]; then
case "${EVENT-message}" in
delta) printf '%s' "$DATA" | jq -j '.text // ""' ;;
job) printf '%s' "$DATA" | jq -c '{job: .}' >&2 ;;
done|pending) printf '%s' "$DATA" | jq -c '{status, job_id, charged_credits, truncated}' >&2
printf '%s' "$DATA" | jq -r '.output.output // empty' ;;
error) printf '%s' "$DATA" | jq -c '{error: .}' >&2 ;;
esac
fi
EVENT=message; DATA="" ;;
esac
done
# A "pending" payload has no output yet: poll /jobs/{job_id} as in step 5.def run_stream(inp, key, on_delta=lambda text: None):
"""POST /run-stream; returns the final payload {job_id, status, charged_credits, output, truncated}."""
body = json.dumps(inp)
headers = {"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN, "Idempotency-Key": key}
with requests.post(BASE + "/run-stream", data=body, headers=headers, stream=True, timeout=300) as r:
if "text/event-stream" not in r.headers.get("content-type", ""):
# An idempotent replay (or an error) comes back as a plain {data} / {error} envelope.
try:
env = r.json()
except ValueError:
env = {}
if not r.ok:
err = env.get("error") or {}
raise ApiError(r.status_code, err.get("code"), err.get("message") or r.reason, err.get("details"))
return env.get("data")
final, failure, event, data = None, None, "message", ""
for line in r.iter_lines(decode_unicode=True):
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data += line[5:].strip()
elif line == "":
if data:
payload = json.loads(data)
if event == "delta":
on_delta(payload.get("text", ""))
elif event == "job":
print("job:", payload)
elif event in ("done", "pending"):
final = payload
elif event == "error":
failure = payload
event, data = "message", ""
if failure:
raise RuntimeError(f"{failure.get('code')}: {failure.get('message')} (job {failure.get('job_id')})")
if final is None:
raise RuntimeError("the stream ended without a done event")
if final.get("status") not in ("succeeded", "failed") and final.get("job_id"):
final = wait_for_job(final["job_id"]) # "pending": the job outlived the stream
return final
streamed = []
final = run_stream(write_input, idem_key("write", write_input), streamed.append)
if final.get("status") == "failed":
raise RuntimeError(f"job failed: {final.get('error')}")
# Browsers may get only heartbeats and the final payload, so read its output first.
text = ((final.get("output") or {}).get("output")) or "".join(streamed)
reply = json.loads(text)
print(reply["headline"], "| charged_credits:", final.get("charged_credits"), "| truncated:", final.get("truncated") is True)async function runStream(input, key, onDelta = () => {}) {
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": "Bearer " + token, "Idempotency-Key": key },
body: JSON.stringify(input),
});
if (!(res.headers.get("content-type") || "").includes("text/event-stream")) {
// An idempotent replay (or an error) comes back as a plain {data} / {error} envelope.
const env = await res.json().catch(() => ({}));
if (!res.ok) {
throw Object.assign(new Error((env.error && env.error.message) || res.statusText),
{ status: res.status, code: env.error && env.error.code, details: env.error && env.error.details });
}
return env.data;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "", result = null, failure = null;
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let idx;
while ((idx = buffer.indexOf("\n\n")) >= 0) {
const raw = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
let event = "message", data = "";
for (const line of raw.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (!data) continue;
let payload;
try { payload = JSON.parse(data); } catch { continue; }
if (event === "delta") onDelta(payload.text || "");
else if (event === "job") console.log("job:", payload);
else if (event === "done" || event === "pending") result = payload;
else if (event === "error") failure = payload;
}
}
if (failure) throw Object.assign(new Error(failure.message || "job failed"), { code: failure.code, job_id: failure.job_id });
if (!result) throw new Error("the stream ended without a done event");
if (result.status !== "succeeded" && result.status !== "failed" && result.job_id) {
return waitForJob(result.job_id); // "pending": the job outlived the stream
}
return result;
}
let streamed = "";
const result = await runStream(writeInput, await idemKey("write", writeInput), (t) => { streamed += t; });
if (result.status === "failed") throw new Error("job failed: " + JSON.stringify(result.error));
// Browsers may get only heartbeats and the final payload, so read its output first.
const streamReply = JSON.parse((result.output && result.output.output) || streamed);
console.log(streamReply.headline, "| charged_credits:", result.charged_credits, "| truncated:", result.truncated === true);// --- package level ---
// runStream posts to /run-stream and returns the final payload
// {job_id, status, charged_credits, output, truncated}.
func runStream(input map[string]any, key string) (map[string]any, error) {
b, err := json.Marshal(input)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(b))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if !strings.Contains(resp.Header.Get("Content-Type"), "text/event-stream") {
// An idempotent replay (or an error) comes back as a plain {data} / {error} envelope.
var env struct {
Data map[string]any `json:"data"`
Error *APIError `json:"error"`
}
_ = json.NewDecoder(resp.Body).Decode(&env)
if resp.StatusCode < 200 || resp.StatusCode > 299 {
if env.Error == nil {
env.Error = &APIError{Message: resp.Status}
}
env.Error.Status = resp.StatusCode
return nil, env.Error
}
return env.Data, nil
}
var final, failure map[string]any
event, data := "message", ""
sc := bufio.NewScanner(resp.Body)
sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024) // the done payload carries the whole reply
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
data += strings.TrimSpace(line[5:])
case line == "":
if data != "" {
var p map[string]any
if json.Unmarshal([]byte(data), &p) == nil {
switch event {
case "delta":
text, _ := p["text"].(string)
fmt.Print(text)
case "job":
fmt.Fprintln(os.Stderr, "job:", p)
case "done", "pending":
final = p
case "error":
failure = p
}
}
}
event, data = "message", ""
}
}
if err := sc.Err(); err != nil {
return nil, err
}
if failure != nil {
return nil, fmt.Errorf("%v: %v (job %v)", failure["code"], failure["message"], failure["job_id"])
}
if final == nil {
return nil, fmt.Errorf("the stream ended without a done event")
}
if s, _ := final["status"].(string); s != "succeeded" && s != "failed" {
if id, ok := final["job_id"].(string); ok {
return waitForJob(id) // "pending": the job outlived the stream
}
}
return final, nil
}
// --- in main ---
final, err := runStream(input, idemKey("write", input, 1))
if err != nil {
panic(err)
}
streamReply, err := replyOf(final) // reads final.output.output, as in step 5
if err != nil {
panic(err)
}
fmt.Println("\n", streamReply["headline"], "| charged_credits:", final["charged_credits"], "| truncated:", final["truncated"] == true)// --- static method in class Api ---
/** POST /run-stream; returns the final payload {job_id, status, charged_credits, output, truncated}. */
static JsonNode runStream(Object input, String key) throws Exception {
HttpRequest req = request("POST", "/run-stream", input, Map.of("Idempotency-Key", key)).build();
HttpResponse<Stream<String>> res = HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
if (!res.headers().firstValue("content-type").orElse("").contains("text/event-stream")) {
// An idempotent replay (or an error) comes back as a plain {data} / {error} envelope.
String raw = res.body().collect(Collectors.joining("\n"));
JsonNode env;
try { env = JSON.readTree(raw); } catch (Exception e) { env = null; }
if (res.statusCode() < 200 || res.statusCode() > 299) throw apiError(res.statusCode(), env);
return env == null ? JSON.missingNode() : env.path("data");
}
JsonNode fin = null, failure = null;
String event = "message";
StringBuilder data = new StringBuilder();
Iterator<String> lines = res.body().iterator();
while (lines.hasNext()) {
String line = lines.next();
if (line.startsWith("event:")) {
event = line.substring(6).trim();
} else if (line.startsWith("data:")) {
data.append(line.substring(5).trim());
} else if (line.isEmpty()) {
if (data.length() > 0) {
JsonNode p = JSON.readTree(data.toString());
switch (event) {
case "delta" -> System.out.print(p.path("text").asText(""));
case "job" -> System.err.println("job: " + p);
case "done", "pending" -> fin = p;
case "error" -> failure = p;
default -> { }
}
}
event = "message";
data.setLength(0);
}
}
if (failure != null) {
throw new IllegalStateException(failure.path("code").asText() + ": "
+ failure.path("message").asText("job failed") + " (job " + failure.path("job_id").asText() + ")");
}
if (fin == null) throw new IllegalStateException("the stream ended without a done event");
String st = fin.path("status").asText("");
if (!st.equals("succeeded") && !st.equals("failed") && fin.hasNonNull("job_id")) {
fin = waitForJob(fin.path("job_id").asText()); // "pending": the job outlived the stream
}
return fin;
}
// --- in main ---
JsonNode fin = runStream(input, idemKey("write", input, 1));
if (fin.path("status").asText().equals("failed")) throw new IllegalStateException("job failed: " + fin.path("error"));
JsonNode streamReply = JSON.readTree(fin.path("output").path("output").asText());
System.out.println();
System.out.println(streamReply.path("headline").asText() + " | charged_credits: " + fin.path("charged_credits")
+ " | truncated: " + fin.path("truncated").asBoolean(false));# POST /run-stream; returns the final payload {job_id, status, charged_credits, output, truncated}.
def run_stream(input, key)
uri, req = build_request("POST", "/run-stream", input, { "Idempotency-Key" => key })
final = failure = nil
Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 300) do |h|
h.request(req) do |res|
unless res["content-type"].to_s.include?("text/event-stream")
# An idempotent replay (or an error) comes back as a plain {data} / {error} envelope.
env = (JSON.parse(res.read_body.to_s) rescue {})
unless res.is_a?(Net::HTTPSuccess)
err = env["error"] || {}
raise ApiError.new(res.code.to_i, err["code"], err["message"] || res.message, err["details"])
end
final = env["data"]
next
end
buffer = +""
res.read_body do |chunk|
buffer << chunk
while (i = buffer.index("\n\n"))
raw = buffer.slice!(0, i + 2)
event = "message"
data = +""
raw.split("\n").each do |line|
if line.start_with?("event:")
event = line[6..].strip
elsif line.start_with?("data:")
data << line[5..].strip
end
end
next if data.empty?
payload = JSON.parse(data)
case event
when "delta" then print payload["text"].to_s
when "job" then warn "job: #{payload}"
when "done", "pending" then final = payload
when "error" then failure = payload
end
end
end
end
end
raise "#{failure["code"]}: #{failure["message"]} (job #{failure["job_id"]})" if failure
raise "the stream ended without a done event" unless final
if !%w[succeeded failed].include?(final["status"]) && final["job_id"]
final = wait_for_job(final["job_id"]) # "pending": the job outlived the stream
end
final
end
final = run_stream(write_input, idem_key("write", write_input))
raise "job failed: #{final["error"]}" if final["status"] == "failed"
stream_reply = JSON.parse(final.dig("output", "output").to_s)
puts "", "#{stream_reply["headline"]} | charged_credits: #{final["charged_credits"]} | truncated: #{final["truncated"] == true}"/** POST /run-stream; returns the final payload {job_id, status, charged_credits, output, truncated}. */
function runStream(array $input, string $key): array {
global $token;
$buffer = ""; $plain = ""; $isSse = null; $final = null; $failure = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json", "Authorization: Bearer $token", "Idempotency-Key: $key"],
CURLOPT_POSTFIELDS => json_encode($input, JSON_UNESCAPED_SLASHES),
CURLOPT_TIMEOUT => 300,
CURLOPT_WRITEFUNCTION => function ($ch, string $chunk) use (&$buffer, &$plain, &$isSse, &$final, &$failure): int {
$isSse ??= str_contains((string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE), "text/event-stream");
if (!$isSse) { $plain .= $chunk; return strlen($chunk); }
$buffer .= $chunk;
while (($i = strpos($buffer, "\n\n")) !== false) {
$raw = substr($buffer, 0, $i);
$buffer = substr($buffer, $i + 2);
$event = "message"; $data = "";
foreach (explode("\n", $raw) as $line) {
if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
elseif (str_starts_with($line, "data:")) $data .= trim(substr($line, 5));
}
$payload = $data === "" ? null : json_decode($data, true);
if (!is_array($payload)) continue;
switch ($event) {
case "delta": echo $payload["text"] ?? ""; break;
case "job": fwrite(STDERR, "job: " . json_encode($payload) . "\n"); break;
case "done": case "pending": $final = $payload; break;
case "error": $failure = $payload; break;
}
}
return strlen($chunk);
},
]);
if (curl_exec($ch) === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if (!$isSse) { // an idempotent replay (or an error) is a plain {data} / {error} envelope
$env = json_decode($plain, true) ?: [];
if ($status < 200 || $status > 299) throw apiError($status, $env);
$final = $env["data"] ?? null;
}
if ($failure) {
throw new RuntimeException(($failure["code"] ?? "") . ": " . ($failure["message"] ?? "job failed") . " (job " . ($failure["job_id"] ?? "") . ")");
}
if (!$final) throw new RuntimeException("the stream ended without a done event");
if (!in_array($final["status"] ?? "", ["succeeded", "failed"], true) && isset($final["job_id"])) {
$final = waitForJob($final["job_id"]); // "pending": the job outlived the stream
}
return $final;
}
$final = runStream($writeInput, idemKey("write", $writeInput));
if (($final["status"] ?? "") === "failed") throw new RuntimeException("job failed: " . json_encode($final["error"] ?? null));
$streamReply = json_decode($final["output"]["output"] ?? "", true, 512, JSON_THROW_ON_ERROR);
echo "\n", $streamReply["headline"], " | charged_credits: ", $final["charged_credits"] ?? "?", "\n";// POST /run-stream; returns the final payload {job_id, status, charged_credits, output, truncated}.
async Task<JsonNode> RunStream(JsonNode input, string key)
{
using var req = Build(HttpMethod.Post, "/run-stream", input,
new Dictionary<string, string> { ["Idempotency-Key"] = key });
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
if (res.Content.Headers.ContentType?.MediaType != "text/event-stream")
{
// An idempotent replay (or an error) comes back as a plain {data} / {error} envelope.
JsonNode? env = null;
try { env = JsonNode.Parse(await res.Content.ReadAsStringAsync()); } catch (JsonException) { }
if (!res.IsSuccessStatusCode) throw ApiErr((int)res.StatusCode, env, res.ReasonPhrase);
return env!["data"]!;
}
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
JsonNode? fin = null, failure = null;
var ev = "message";
var data = new StringBuilder();
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:")) data.Append(line[5..].Trim());
else if (line.Length == 0)
{
if (data.Length > 0)
{
var p = JsonNode.Parse(data.ToString());
switch (ev)
{
case "delta": Console.Write(p?["text"]?.GetValue<string>()); break;
case "job": Console.Error.WriteLine($"job: {p?.ToJsonString()}"); break;
case "done": case "pending": fin = p; break;
case "error": failure = p; break;
}
}
ev = "message";
data.Clear();
}
}
if (failure != null) throw new Exception($"{failure["code"]}: {failure["message"]} (job {failure["job_id"]})");
if (fin == null) throw new Exception("the stream ended without a done event");
if (fin["status"]?.GetValue<string>() is not ("succeeded" or "failed") && fin["job_id"] != null)
fin = await WaitForJob(fin["job_id"]!.GetValue<string>()); // "pending": the job outlived the stream
return fin;
}
var streamResult = await RunStream(input, IdemKey("write", input));
if (streamResult["status"]?.GetValue<string>() == "failed") throw new Exception($"job failed: {streamResult["error"]?.ToJsonString()}");
var streamReply = JsonNode.Parse(streamResult["output"]!["output"]!.GetValue<string>())!;
Console.WriteLine();
Console.WriteLine($"{streamReply["headline"]} | charged_credits: {streamResult["charged_credits"]} | truncated: {streamResult["truncated"]}");