Drive Band Name Generator from your own code
Everything the web app does goes through one public surface. Base URL:
https://api.skillsafe.ai/v1/app-api
Every request carries Authorization: Bearer <token> and
Content-Type: application/json. Every response is a JSON envelope:
{"ok":true,"data":{...}} on success and
{"ok":false,"error":{"code":"...","message":"..."}} on failure. Read
error.code, not the HTTP status alone.
The one thing to know before you automate this
The corpus check that makes this app worth using runs in the browser, not on the
server. The API returns the model's own taken_risk guess and nothing
more. If you drive this from code, you are responsible for your own collision checking, and
taken_risk must never be treated as an availability signal.
Errors
| Code | Meaning | What to do |
|---|---|---|
UNAUTHORIZED | Missing, malformed or expired bearer token. | Mint a fresh token from the token page, or sign in again. |
FORBIDDEN | The token is valid but not for this app. | Tokens are per-app. Use one issued for band-name-generator. |
INSUFFICIENT_CREDITS | Balance is below min_credits for this run. | Top up. Between min_credits and hold_credits the run still executes with a reduced cap and returns truncated:true. |
VALIDATION_ERROR | The request body was rejected. | Check error.details. Note /estimate does NOT validate -- only /run and /run-stream do. |
RATE_LIMITED | Too many requests. | Back off and retry; do not tight-loop. |
JOB_NOT_FOUND | Unknown job id on /jobs/{id}. | Job ids are per-subject; confirm the same token that submitted it is polling. |
INTERNAL | Server-side failure. | Retry once with the SAME Idempotency-Key so a partial charge is not repeated. |
The input object
Taken from app.js, which is what the web app actually submits.
| Field | Type | Notes |
|---|---|---|
task | string | Either "names" (a first batch) or "more" (a follow-up). |
genre | string | Required. Free text; matched to one of 22 genre families, longest alias wins. |
mood | string | Free text. The emotional register the names should sit in. |
reference_acts | string[] | Up to 6. A direction, not a template -- echoing them is treated as a failure. |
avoid_words | string[] | Up to 12 words to keep out of the names. |
count | number | 3, 5 or 7. Always smaller than the device inventory, so covering the set is impossible. |
brief.device_inventory | object[] | The genre's naming devices: id, label, hint, theme, shape. |
brief.suggested_devices | string[] | Three ids drawn at random. Explicitly optional. |
brief.avoid_openings | string[] | Openings already used this session, two content words deep. |
previous_names | string[] | Only on task "more". The names already produced. |
{
"task": "names",
"genre": "black metal",
"mood": "funereal, remote, cold",
"reference_acts": [],
"avoid_words": [],
"count": 5,
"brief": {
"genre_id": "black_metal",
"genre_label": "black metal",
"register": "Cold, remote and unwelcoming...",
"avoid": "Anything cute, colloquial...",
"device_inventory": [
{
"id": "bm_nordic",
"label": "a Norwegian or Icelandic compound",
"hint": "one compound word from a Nordic language, left untranslated",
"theme": "geography",
"shape": "foreign"
}
],
"suggested_devices": [
"bm_nordic",
"bm_cosmo",
"bm_archaic"
],
"count": 5,
"seed": 123456,
"avoid_openings": []
}
}
1. A tiny client helper
Every call is a POST with a bearer token and a JSON body, and every response is the same {ok, data, error} envelope. Wrap that once.
# A shell helper. Keep the token out of your history: read it from a file
# or an exported variable rather than pasting it into the command.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"
call() { # call <path> <json-body>
curl -sS -X POST "$BASE$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
}
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("BAND_NAME_LAB_TOKEN", "YOUR_TOKEN")
def call(path, body=None, method="POST"):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
payload = json.load(r)
if not payload.get("ok"):
raise RuntimeError(payload["error"]["code"] + ": " + payload["error"]["message"])
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
async function call(path, body, method = "POST") {
const res = await fetch(BASE + path, {
method,
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const payload = await res.json();
if (!payload.ok) {
throw new Error(`${payload.error.code}: ${payload.error.message}`);
}
return payload.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body any) (json.RawMessage, error) {
token := os.Getenv("BAND_NAME_LAB_TOKEN")
if token == "" {
token = "YOUR_TOKEN"
}
var buf io.Reader
if body != nil {
b, _ := json.Marshal(body)
buf = bytes.NewReader(b)
}
req, _ := http.NewRequest("POST", base+path, buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s: %s", e.Error.Code, e.Error.Message)
}
return e.Data, nil
}
import java.net.URI;
import java.net.http.*;
import java.util.Optional;
public class BandNameGenerator {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN =
Optional.ofNullable(System.getenv("BAND_NAME_LAB_TOKEN")).orElse("YOUR_TOKEN");
static final HttpClient CLIENT = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
jsonBody == null ? "{}" : jsonBody))
.build();
HttpResponse<String> res =
CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
return res.body(); // parse the {ok,data,error} envelope with your JSON library
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("BAND_NAME_LAB_TOKEN", "YOUR_TOKEN")
def call(path, body = nil)
uri = URI(BASE + path)
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(body) unless body.nil?
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";
function call(string $path, ?array $body = null): array {
$token = getenv("BAND_NAME_LAB_TOKEN") ?: "YOUR_TOKEN";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => $body === null ? "{}" : json_encode($body),
]);
$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;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public static class BandNameGenerator {
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token =
Environment.GetEnvironmentVariable("BAND_NAME_LAB_TOKEN") ?? "YOUR_TOKEN";
static readonly HttpClient Client = new HttpClient();
public static async Task<JsonElement> CallAsync(string path, object body = null) {
var req = new HttpRequestMessage(HttpMethod.Post, Base + path);
req.Headers.Add("Authorization", "Bearer " + Token);
req.Content = new StringContent(
body == null ? "{}" : JsonSerializer.Serialize(body),
Encoding.UTF8, "application/json");
var res = await Client.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
var root = doc.RootElement;
if (!root.GetProperty("ok").GetBoolean()) {
var err = root.GetProperty("error");
throw new Exception(err.GetProperty("code").GetString() + ": " +
err.GetProperty("message").GetString());
}
return root.GetProperty("data");
}
}
2. Check who you are with /me
Confirms the token works and reports the balance. /me carries exactly three fields: subject_type, subject_id and credits. Signed in means subject_type is "user"; a guest token also returns 200 here, so read the field.
curl -sS "https://api.skillsafe.ai/v1/app-api/me" -H "Authorization: Bearer $TOKEN"
# {"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":48210}}
# subject_type is "user" when signed in and "guest" otherwise. Those three
# fields are the whole of /me -- there is no email, name or plan on it.
me = call("/me", method="GET")
print(me["subject_type"], me["credits"])
# Signed in means subject_type == "user". A guest token also succeeds here,
# so check the field rather than treating a 200 as proof of sign-in.
const me = await call("/me", undefined, "GET");
console.log(me.subject_type, me.credits);
// Signed in means subject_type === "user"; a guest token also gets a 200.
data, err := call("/me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Credits int `json:"credits"`
}
json.Unmarshal(data, &me)
fmt.Println(me.SubjectType, me.Credits)
String body = call("/me", null);
// {"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":48210}}
// Parse the envelope, then read data.subject_type and data.credits.
System.out.println(body);
me = call("/me")
puts "#{me["subject_type"]} #{me["credits"]}"
# subject_type is "user" when signed in, "guest" otherwise.
<?php
$me = call("/me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
// subject_type is "user" when signed in, "guest" otherwise.
var me = await BandNameGenerator.CallAsync("/me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
Console.WriteLine(me.GetProperty("credits").GetInt32());
3. Price it with /estimate
Free, and it creates no job. It returns the model binding and a hold_credits reservation priced at the full output cap — not the price. It performs no validation whatsoever on the body, so a clean estimate proves the model binding and nothing about your input shape.
call /estimate '{"task": "names", "genre": "black metal", "mood": "funereal, remote, cold", "count": 5, "brief": {"device_inventory": [{"id": "bm_nordic"}]}}'
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":3120,"min_credits":640}}
#
# WARNING: /estimate does NO validation on the body. A bare string, a null or
# an empty array all return ok:true with a plausible hold. A clean estimate
# proves the model binding, NOT that your input shape is right.
est = call("/estimate", run_input)
print(est["hold_credits"], est["model"], est["model_alias"])
# hold_credits is a RESERVATION priced at the full output cap, not the price.
# What is actually charged is usually much lower and comes back on the job.
#
# /estimate validates nothing: passing "" or None or [] still returns ok:true
# with a sensible-looking hold. Validate the object yourself before sending.
const est = await call("/estimate", runInput);
console.log(est.hold_credits, est.model, est.model_alias);
// hold_credits is a reservation priced at the full output cap, not the price.
// /estimate performs no validation on the body -- a bare string returns ok:true
// with a plausible hold -- so check your own input shape before calling it.
data, err := call("/estimate", runInput)
if err != nil {
panic(err)
}
var est struct {
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
MarkupBps int `json:"markup_bps"`
HoldCredits int `json:"hold_credits"`
MinCredits int `json:"min_credits"`
}
json.Unmarshal(data, &est)
fmt.Println(est.HoldCredits, est.Model)
String est = call("/estimate", runInputJson);
// data.hold_credits is a reservation at the full output cap, not the price.
// data.model is "gpt-5.6-terra"; data.model_alias is "gpt-terra".
System.out.println(est);
est = call("/estimate", run_input)
puts "#{est["hold_credits"]} #{est["model"]}"
# hold_credits is a reservation, not the price. Note /estimate does not
# validate the body at all -- verify your own input before sending it.
<?php
$est = call("/estimate", $runInput);
echo $est["hold_credits"], " ", $est["model"], PHP_EOL;
// hold_credits reserves the full output cap; the charge is usually lower.
var est = await BandNameGenerator.CallAsync("/estimate", runInput);
Console.WriteLine(est.GetProperty("hold_credits").GetInt32());
Console.WriteLine(est.GetProperty("model").GetString());
4. Run it and poll
Submit to /run, then poll /jobs/{job_id} until status is succeeded or failed. Always send an Idempotency-Key so a network blip cannot double-bill.
# Submit, then poll. Send an Idempotency-Key so a retry cannot double-bill.
JOB=$(curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: band-name-generator:names:$(date +%s)" \
-d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
while :; do
R=$(curl -sS "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" -H "Authorization: Bearer $TOKEN")
S=$(printf '%s' "$R" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
[ "$S" = "succeeded" ] || [ "$S" = "failed" ] && break
sleep 1
done
printf '%s' "$R"
import time, uuid
job = call("/run", run_input) # add an Idempotency-Key header in real use
job_id = job["job_id"]
while True:
j = call("/jobs/" + job_id, method="GET")
if j["status"] in ("succeeded", "failed"):
break
time.sleep(1)
result = json.loads(j["output"]["output"]) # the model's JSON object
print(result["names"][0]["name"], j.get("charged_credits"))
const job = await call("/run", runInput); // send an Idempotency-Key in real use
let j;
for (;;) {
j = await call(`/jobs/${job.job_id}`, undefined, "GET");
if (j.status === "succeeded" || j.status === "failed") break;
await new Promise(r => setTimeout(r, 1000));
}
const result = JSON.parse(j.output.output);
console.log(result.names[0].name, j.charged_credits);
data, _ := call("/run", runInput)
var job struct {
JobID string `json:"job_id"`
}
json.Unmarshal(data, &job)
for {
d, err := call("/jobs/"+job.JobID, nil)
if err != nil {
panic(err)
}
var j struct {
Status string `json:"status"`
ChargedCredits int `json:"charged_credits"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
json.Unmarshal(d, &j)
if j.Status == "succeeded" || j.Status == "failed" {
fmt.Println(j.Status, j.Output.Output)
break
}
time.Sleep(time.Second)
}
String submitted = call("/run", runInputJson);
// read data.job_id from the envelope, then poll:
// GET /jobs/{job_id} until data.status is "succeeded" or "failed"
// data.output.output holds the model's JSON object as a string.
// Always send an Idempotency-Key header so a retry cannot double-bill.
System.out.println(submitted);
job = call("/run", run_input) # send an Idempotency-Key header in real use
loop do
@j = call("/jobs/#{job["job_id"]}")
break if %w[succeeded failed].include?(@j["status"])
sleep 1
end
result = JSON.parse(@j["output"]["output"])
puts result["names"].first["name"]
<?php
$job = call("/run", $runInput); // send an Idempotency-Key header in real use
do {
sleep(1);
$j = call("/jobs/" . $job["job_id"]);
} while (!in_array($j["status"], ["succeeded", "failed"], true));
$result = json_decode($j["output"]["output"], true);
echo $result["names"][0]["name"], PHP_EOL;
var job = await BandNameGenerator.CallAsync("/run", runInput);
var jobId = job.GetProperty("job_id").GetString();
JsonElement j;
while (true) {
j = await BandNameGenerator.CallAsync("/jobs/" + jobId);
var status = j.GetProperty("status").GetString();
if (status == "succeeded" || status == "failed") break;
await Task.Delay(1000);
}
var result = JsonDocument.Parse(j.GetProperty("output").GetProperty("output").GetString());
Console.WriteLine(result.RootElement.GetProperty("names")[0].GetProperty("name").GetString());
5. Or stream it with /run-stream
Server-sent events, so you can show progress while the batch is written.
curl -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: band-name-generator:names:run-1" \
-d "$INPUT"
# Wire format -- one event per block, terminated by a blank line:
#
# event: job
# data: {"job_id":"job_..."}
#
# event: delta
# data: {"text":"{\"genre_read\":\"co"}
#
# event: done
# data: {"status":"succeeded","charged_credits":2140,"truncated":false}
#
# Event names are: job, delta, done, pending, error. Concatenate the `text`
# field of every delta event to rebuild the model's JSON object.
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps(run_input).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", "band-name-generator:names:run-1")
event, buf = None, ""
with urllib.request.urlopen(req) as r:
for raw in r:
line = raw.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
payload = json.loads(line[6:])
if event == "delta":
buf += payload["text"]
elif event == "done":
print("charged", payload.get("charged_credits"))
elif line == "":
event = None
result = json.loads(buf)
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": "band-name-generator:names:run-1"
},
body: JSON.stringify(runInput)
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let carry = "", event = null, buf = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
carry += decoder.decode(value, { stream: true });
const lines = carry.split("\n");
carry = lines.pop();
for (const line of lines) {
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ")) {
const payload = JSON.parse(line.slice(6));
if (event === "delta") buf += payload.text;
if (event === "done") console.log("charged", payload.charged_credits);
} else if (line === "") event = null;
}
}
const result = JSON.parse(buf);
body, _ := json.Marshal(runInput)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "band-name-generator:names:run-1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var event, buf string
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimPrefix(line, "event: ")
case strings.HasPrefix(line, "data: "):
var p struct {
Text string `json:"text"`
ChargedCredits int `json:"charged_credits"`
}
json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &p)
if event == "delta" {
buf += p.Text
}
case line == "":
event = ""
}
}
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "band-name-generator:names:run-1")
.POST(HttpRequest.BodyPublishers.ofString(runInputJson))
.build();
StringBuilder buf = new StringBuilder();
String[] event = {null};
CLIENT.send(req, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.startsWith("event: ")) {
event[0] = line.substring(7);
} else if (line.startsWith("data: ") && "delta".equals(event[0])) {
// append the "text" field of the JSON payload to buf
buf.append(extractText(line.substring(6)));
} else if (line.isEmpty()) {
event[0] = null;
}
});
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "band-name-generator:names:run-1"
req.body = JSON.dump(run_input)
event = nil
buf = +""
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: ")
payload = JSON.parse(line[6..])
buf << payload["text"] if event == "delta"
elsif line.empty? then event = nil
end
end
end
end
end
result = JSON.parse(buf)
<?php
$event = null;
$buf = "";
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
"Idempotency-Key: band-name-generator:names:run-1",
],
CURLOPT_POSTFIELDS => json_encode($runInput),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$buf) {
foreach (explode("\n", $chunk) as $line) {
$line = rtrim($line, "\r");
if (str_starts_with($line, "event: ")) {
$event = substr($line, 7);
} elseif (str_starts_with($line, "data: ")) {
$p = json_decode(substr($line, 6), true);
if ($event === "delta") { $buf .= $p["text"]; }
} elseif ($line === "") {
$event = null;
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$result = json_decode($buf, true);
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Add("Authorization", "Bearer " + Token);
req.Headers.Add("Idempotency-Key", "band-name-generator:names:run-1");
req.Content = new StringContent(JsonSerializer.Serialize(runInput),
Encoding.UTF8, "application/json");
var res = await Client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string evt = null;
var buf = new StringBuilder();
while (!reader.EndOfStream) {
var line = await reader.ReadLineAsync();
if (line.StartsWith("event: ")) evt = line.Substring(7);
else if (line.StartsWith("data: ")) {
var p = JsonDocument.Parse(line.Substring(6)).RootElement;
if (evt == "delta") buf.Append(p.GetProperty("text").GetString());
} else if (line.Length == 0) evt = null;
}
var result = JsonDocument.Parse(buf.ToString());
6. Ask for more, without getting variations
A follow-up batch is told what the previous one already did, so it goes somewhere else rather than rewording what you rejected.
# A follow-up batch that avoids what the first one already did.
cat > more.json <<'JSON'
{
"task": "more",
"genre": "black metal",
"mood": "funereal, remote, cold",
"count": 5,
"previous_names": ["Sepulchral Vow", "Frostmere"],
"brief": {
"device_inventory": [{"id": "bm_nordic"}],
"avoid_openings": ["sepulchral vow", "frostmer"]
}
}
JSON
call /run-stream "$(cat more.json)"
# Set task to "more", pass the names you already have in previous_names, and put their openings in brief.avoid_openings. An opening is the first TWO content words, lowercased and lightly stemmed, with articles and prepositions dropped -- so "making a promise" and "making the promises" are the SAME opening. Comparing only first words is too coarse to catch a repeated frame.
previous = [n["name"] for n in result["names"]]
more_input = dict(run_input)
more_input["task"] = "more"
more_input["previous_names"] = previous
more_input["brief"] = dict(run_input["brief"])
more_input["brief"]["avoid_openings"] = [opening_key(n) for n in previous]
more = call("/estimate", more_input) # re-estimate: the shape changed
print(more["hold_credits"])
# Set task to "more", pass the names you already have in previous_names, and put their openings in brief.avoid_openings. An opening is the first TWO content words, lowercased and lightly stemmed, with articles and prepositions dropped -- so "making a promise" and "making the promises" are the SAME opening. Comparing only first words is too coarse to catch a repeated frame.
const previous = result.names.map(n => n.name);
const moreInput = {
...runInput,
task: "more",
previous_names: previous,
brief: { ...runInput.brief, avoid_openings: previous.map(openingKey) }
};
const est = await call("/estimate", moreInput); // re-estimate; the shape changed
// Set task to "more", pass the names you already have in previous_names, and put their openings in brief.avoid_openings. An opening is the first TWO content words, lowercased and lightly stemmed, with articles and prepositions dropped -- so "making a promise" and "making the promises" are the SAME opening. Comparing only first words is too coarse to catch a repeated frame.
moreInput := runInput
moreInput["task"] = "more"
moreInput["previous_names"] = previousNames
brief := moreInput["brief"].(map[string]any)
brief["avoid_openings"] = openings
// Re-estimate whenever the shape changes -- a "more" run prices differently
// from a first run because previous_names is carried in the body.
data, err := call("/estimate", moreInput)
// Set task to "more", pass the names you already have in previous_names, and put their openings in brief.avoid_openings. An opening is the first TWO content words, lowercased and lightly stemmed, with articles and prepositions dropped -- so "making a promise" and "making the promises" are the SAME opening. Comparing only first words is too coarse to catch a repeated frame.
// Build the follow-up body: task "more", previous_names holding the names you
// already received, and brief.avoid_openings holding their openings.
//
// Set task to "more", pass the names you already have in previous_names, and put their openings in brief.avoid_openings. An opening is the first TWO content words, lowercased and lightly stemmed, with articles and prepositions dropped -- so "making a promise" and "making the promises" are the SAME opening. Comparing only first words is too coarse to catch a repeated frame.
String moreJson = buildMoreInput(runInputJson, previousNames);
String est = call("/estimate", moreJson);
previous = result["names"].map { |n| n["name"] }
more_input = run_input.merge(
"task" => "more",
"previous_names" => previous,
"brief" => run_input["brief"].merge("avoid_openings" => previous.map { |n| opening_key(n) })
)
est = call("/estimate", more_input) # re-estimate; the shape changed
# Set task to "more", pass the names you already have in previous_names, and put their openings in brief.avoid_openings. An opening is the first TWO content words, lowercased and lightly stemmed, with articles and prepositions dropped -- so "making a promise" and "making the promises" are the SAME opening. Comparing only first words is too coarse to catch a repeated frame.
<?php
$previous = array_map(fn($n) => $n["name"], $result["names"]);
$moreInput = $runInput;
$moreInput["task"] = "more";
$moreInput["previous_names"] = $previous;
$moreInput["brief"]["avoid_openings"] = array_map("opening_key", $previous);
$est = call("/estimate", $moreInput); // re-estimate; the shape changed
// Set task to "more", pass the names you already have in previous_names, and put their openings in brief.avoid_openings. An opening is the first TWO content words, lowercased and lightly stemmed, with articles and prepositions dropped -- so "making a promise" and "making the promises" are the SAME opening. Comparing only first words is too coarse to catch a repeated frame.
var previous = result.RootElement.GetProperty("names")
.EnumerateArray().Select(n => n.GetProperty("name").GetString()).ToArray();
var moreInput = new Dictionary<string, object>(runInput) {
["task"] = "more",
["previous_names"] = previous
};
// Re-estimate: a "more" run prices differently from a first run.
var est = await BandNameGenerator.CallAsync("/estimate", moreInput);
// Set task to "more", pass the names you already have in previous_names, and put their openings in brief.avoid_openings. An opening is the first TWO content words, lowercased and lightly stemmed, with articles and prepositions dropped -- so "making a promise" and "making the promises" are the SAME opening. Comparing only first words is too coarse to catch a repeated frame.
7. Read the output
One JSON object. Every entry carries the name, the device it used, why it fits, how it behaves as a wordmark, and the model's own risk read.
# What comes back, and what it does not tell you.
{
"genre_read": "one or two sentences on how this genre names itself",
"expected_count": 5,
"names": [
{
"name": "",
"device": "bm_nordic",
"why_it_fits": "",
"on_a_shirt": "",
"taken_risk": "low",
"taken_note": ""
}
],
"notes": ""
}
# The API returns the model's own taken_risk judgement. It does NOT run the corpus check -- that lives in the browser. If you are driving this from code you must do your own collision checking, and you must not treat taken_risk as an availability signal. It is the model's guess.
result = json.loads(job["output"]["output"])
for n in result["names"]:
print(n["name"], "|", n["device"], "|", n["taken_risk"])
print(" ", n["why_it_fits"])
print(" ", n["on_a_shirt"])
# The API returns the model's own taken_risk judgement. It does NOT run the corpus check -- that lives in the browser. If you are driving this from code you must do your own collision checking, and you must not treat taken_risk as an availability signal. It is the model's guess.
const result = JSON.parse(job.output.output);
for (const n of result.names) {
console.log(n.name, "|", n.device, "|", n.taken_risk);
console.log(" ", n.why_it_fits);
console.log(" ", n.on_a_shirt);
}
// The API returns the model's own taken_risk judgement. It does NOT run the corpus check -- that lives in the browser. If you are driving this from code you must do your own collision checking, and you must not treat taken_risk as an availability signal. It is the model's guess.
var result struct {
GenreRead string `json:"genre_read"`
Names []struct {
Name string `json:"name"`
Device string `json:"device"`
WhyItFits string `json:"why_it_fits"`
OnAShirt string `json:"on_a_shirt"`
TakenRisk string `json:"taken_risk"`
TakenNote string `json:"taken_note"`
} `json:"names"`
Notes string `json:"notes"`
}
json.Unmarshal([]byte(jobOutput), &result)
// The API returns the model's own taken_risk judgement. It does NOT run the corpus check -- that lives in the browser. If you are driving this from code you must do your own collision checking, and you must not treat taken_risk as an availability signal. It is the model's guess.
// The model returns a single JSON object:
// {
// "genre_read": "one or two sentences on how this genre names itself",
// "expected_count": 5,
// "names": [
// {
// "name": "",
// "device": "bm_nordic",
// "why_it_fits": "",
// "on_a_shirt": "",
// "taken_risk": "low",
// "taken_note": ""
// }
// ],
// "notes": ""
// }
//
// The API returns the model's own taken_risk judgement. It does NOT run the corpus check -- that lives in the browser. If you are driving this from code you must do your own collision checking, and you must not treat taken_risk as an availability signal. It is the model's guess.
result = JSON.parse(job["output"]["output"])
result["names"].each do |n|
puts "#{n["name"]} | #{n["device"]} | #{n["taken_risk"]}"
puts " #{n["why_it_fits"]}"
puts " #{n["on_a_shirt"]}"
end
# The API returns the model's own taken_risk judgement. It does NOT run the corpus check -- that lives in the browser. If you are driving this from code you must do your own collision checking, and you must not treat taken_risk as an availability signal. It is the model's guess.
<?php
$result = json_decode($job["output"]["output"], true);
foreach ($result["names"] as $n) {
echo $n["name"], " | ", $n["device"], " | ", $n["taken_risk"], PHP_EOL;
echo " ", $n["why_it_fits"], PHP_EOL;
echo " ", $n["on_a_shirt"], PHP_EOL;
}
// The API returns the model's own taken_risk judgement. It does NOT run the corpus check -- that lives in the browser. If you are driving this from code you must do your own collision checking, and you must not treat taken_risk as an availability signal. It is the model's guess.
var result = JsonDocument.Parse(jobOutput).RootElement;
foreach (var n in result.GetProperty("names").EnumerateArray()) {
Console.WriteLine($"{n.GetProperty("name").GetString()} | " +
$"{n.GetProperty("device").GetString()} | " +
$"{n.GetProperty("taken_risk").GetString()}");
}
// The API returns the model's own taken_risk judgement. It does NOT run the corpus check -- that lives in the browser. If you are driving this from code you must do your own collision checking, and you must not treat taken_risk as an availability signal. It is the model's guess.
Output contract
A single JSON object. expected_count equals the count requested, so a
client can detect a truncated reply by comparing it against names.length.
{
"genre_read": "one or two sentences on how this genre names itself",
"expected_count": 5,
"names": [
{
"name": "",
"device": "bm_nordic",
"why_it_fits": "",
"on_a_shirt": "",
"taken_risk": "low",
"taken_note": ""
}
],
"notes": ""
}