API リファレンス
概要
- ベース URL:
https://gyojy.hakadoru.ai/api - 認証: OAuth 2.0 Client Credentials で取得したアクセストークンを
Authorization: Bearer <token>で送る - 本文は JSON(
/oauth/tokenのみapplication/x-www-form-urlencoded)。本文の上限は 5MB - すべての応答に
X-Request-Idヘッダが付きます。問い合わせの際はこの値を添えてください - 費用は推論の前に残高から差し引かれ、サービス側の都合で失敗したとき(
no_label/busy/upstream_error)は返金されます
API クライアント(client_id / client_secret)はダッシュボードで発行します。
client_secret は発行時に 1 回だけ表示されるので、その場で安全な場所に保存してください。
クイックスタート
トークン取得 → 画像つきの判断(/v1/judge)→ 2 枚の比較(/v1/compare)までを通しで行う例です。
環境変数 GVJ_CLIENT_ID / GVJ_CLIENT_SECRET に資格情報を入れてから実行してください。
bash(curl・jq・ImageMagick を使用)
API=https://gyojy.hakadoru.ai/api
# ダッシュボードで発行した資格情報(Secret は発行時に 1 回だけ表示される)
export GVJ_CLIENT_ID=gvj_ci_xxxxxxxx
export GVJ_CLIENT_SECRET=gvj_cs_xxxxxxxx
# 1. アクセストークンを取得(HTTP Basic、有効期限 1 時間)
TOKEN=$(curl -sS -u "$GVJ_CLIENT_ID:$GVJ_CLIENT_SECRET" \
-d grant_type=client_credentials -d scope=judge \
"$API/oauth/token" | jq -r .access_token)
# 1.5 推論サーバを起こしておく(無料)。止まっていれば起動が始まり、1〜2 分で使えるようになる
curl -sS -X POST "$API/v1/warmup" -H "Authorization: Bearer $TOKEN"
for i in $(seq 18); do # 最大 3 分
[ "$(curl -sS "$API/v1/status" -H "Authorization: Bearer $TOKEN" | jq -r .worker)" = ready ] && break
sleep 10
done
# 2. 画像つきの判断(/v1/judge)
# 長辺 1024px に縮小してから送る(ImageMagick 7。'>' は大きいときだけ縮小)
magick photo.jpg -auto-orient -resize '1024x1024>' -quality 90 photo-1024.jpg
# base64 は長いので、コマンド引数ではなくファイル経由で JSON に埋め込む
base64 < photo-1024.jpg | tr -d '\n' > photo.b64
jq -n --rawfile img photo.b64 '{
images: ["data:image/jpeg;base64," + $img],
questions: {
season: {
type: "choice",
instructions: "この写真の季節として最も適切なのは?",
criteria: {"春": null, "夏": null, "秋": "紅葉や落ち葉が見える", "冬": null}
}
}
}' > judge.json
curl -sS "$API/v1/judge" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @judge.json | jq .
# 3. 2 枚の比較(/v1/compare)
base64 < a.jpg | tr -d '\n' > a.b64
base64 < b.jpg | tr -d '\n' > b.b64
jq -n --rawfile a a.b64 --rawfile b b.b64 '{
images: {A: ("data:image/jpeg;base64," + $a), B: ("data:image/jpeg;base64," + $b)},
criterion: "判断基準: より明るい画像が勝ち",
tie: false
}' > compare.json
curl -sS "$API/v1/compare" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @compare.json | jq '{verdict, consistent, probabilities, credits: .usage.credits}'
gvj.py(requests)
# Python 3.9+ / pip install requests
import base64
import mimetypes
import os
import time
import requests
API = "https://gyojy.hakadoru.ai/api"
def get_token() -> str:
"""client_credentials でアクセストークンを取得する(有効期限 1 時間。期限内は使い回す)。"""
r = requests.post(
f"{API}/oauth/token",
auth=(os.environ["GVJ_CLIENT_ID"], os.environ["GVJ_CLIENT_SECRET"]), # HTTP Basic
data={"grant_type": "client_credentials", "scope": "judge"},
timeout=30,
)
r.raise_for_status()
return r.json()["access_token"]
def data_url(path: str) -> str:
"""画像ファイルを data: URL にする。送る前に長辺 1024px へ縮小しておくこと。"""
mime = mimetypes.guess_type(path)[0] or "image/jpeg"
with open(path, "rb") as f:
return f"data:{mime};base64," + base64.b64encode(f.read()).decode("ascii")
def call(token: str, path: str, body: dict, attempts: int = 6) -> dict:
for _ in range(attempts):
r = requests.post(
f"{API}{path}",
json=body,
headers={"Authorization": f"Bearer {token}"},
timeout=120,
)
# 推論サーバが停止中(warming_up)・混雑(busy)なら課金されていないので、待って送り直す
if r.status_code == 503:
time.sleep(int(r.headers.get("Retry-After", "30")))
continue
break
if not r.ok:
try:
err = r.json()["error"]
except (ValueError, KeyError, TypeError):
err = {"code": f"http_{r.status_code}", "message": r.text[:200]}
request_id = r.headers.get("X-Request-Id")
raise RuntimeError(f"{r.status_code} {err['code']}: {err['message']} (X-Request-Id: {request_id})")
return r.json()
token = get_token()
# 画像つきの判断(/v1/judge)
judged = call(token, "/v1/judge", {
"images": [data_url("photo-1024.jpg")],
"questions": {
"season": {
"type": "choice",
"instructions": "この写真の季節として最も適切なのは?",
"criteria": {"春": None, "夏": None, "秋": "紅葉や落ち葉が見える", "冬": None},
}
},
})
ans = judged["answers"]["season"]
print(ans["choice"], ans["probabilities"], "coverage:", ans["coverage"], "credits:", judged["usage"]["credits"])
# 2 枚の比較(/v1/compare)
cmp = call(token, "/v1/compare", {
"images": {"A": data_url("a.jpg"), "B": data_url("b.jpg")},
"criterion": "判断基準: より明るい画像が勝ち",
"tie": False,
})
print(cmp["verdict"], cmp["consistent"], cmp["probabilities"])
gvj.mts(Node.js 20+、fetch)
// Node.js 20 以降(fetch を標準搭載)。実行例: npx tsx gvj.mts
import { readFile } from "node:fs/promises";
import { extname } from "node:path";
const API = "https://gyojy.hakadoru.ai/api";
type Answer = {
type: "choice";
choice: string;
probabilities: Record<string, number>;
confidence: number;
coverage: number;
};
type Usage = { input_tokens: number; output_tokens: number; elapsed_ms: number; credits: number };
type JudgeResponse = { model: string; served_by: string; answers: Record<string, Answer>; usage: Usage };
type CompareResponse = {
verdict: "A" | "B" | "tie" | "undecided";
consistent: boolean;
probabilities: Record<string, number>;
runs: { order: string[]; probabilities: Record<string, number> }[];
usage: Usage;
};
/** client_credentials でアクセストークンを取得する(有効期限 1 時間。期限内は使い回す)。 */
async function getToken(): Promise<string> {
const id = process.env.GVJ_CLIENT_ID ?? "";
const secret = process.env.GVJ_CLIENT_SECRET ?? "";
const res = await fetch(`${API}/oauth/token`, {
method: "POST",
headers: { Authorization: `Basic ${Buffer.from(`${id}:${secret}`).toString("base64")}` },
body: new URLSearchParams({ grant_type: "client_credentials", scope: "judge" }),
});
if (!res.ok) throw new Error(`token: ${res.status} ${await res.text()}`);
const json = (await res.json()) as { access_token: string; expires_in: number };
return json.access_token;
}
const MIME: Record<string, string> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
".gif": "image/gif",
};
/** 画像ファイルを data: URL にする。送る前に長辺 1024px へ縮小しておくこと。 */
async function dataUrl(path: string): Promise<string> {
const mime = MIME[extname(path).toLowerCase()] ?? "image/jpeg";
return `data:${mime};base64,${(await readFile(path)).toString("base64")}`;
}
async function call<T>(token: string, path: string, body: unknown, attempts = 6): Promise<T> {
let res: Response;
for (let i = 0; ; i++) {
res = await fetch(`${API}${path}`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
// 推論サーバが停止中(warming_up)・混雑(busy)なら課金されていないので、待って送り直す
if (res.status !== 503 || i + 1 >= attempts) break;
const wait = Number(res.headers.get("retry-after") ?? "30");
await new Promise((r) => setTimeout(r, wait * 1000));
}
if (!res.ok) {
const requestId = res.headers.get("x-request-id");
throw new Error(`${path}: ${res.status} ${await res.text()} (X-Request-Id: ${requestId})`);
}
return (await res.json()) as T;
}
const token = await getToken();
// 画像つきの判断(/v1/judge)
const judged = await call<JudgeResponse>(token, "/v1/judge", {
images: [await dataUrl("photo-1024.jpg")],
questions: {
season: {
type: "choice",
instructions: "この写真の季節として最も適切なのは?",
criteria: { 春: null, 夏: null, 秋: "紅葉や落ち葉が見える", 冬: null },
},
},
});
const ans = judged.answers.season;
console.log(ans.choice, ans.probabilities, "coverage:", ans.coverage, "credits:", judged.usage.credits);
// 2 枚の比較(/v1/compare)
const cmp = await call<CompareResponse>(token, "/v1/compare", {
images: { A: await dataUrl("a.jpg"), B: await dataUrl("b.jpg") },
criterion: "判断基準: より明るい画像が勝ち",
tie: false,
});
console.log(cmp.verdict, cmp.consistent, cmp.probabilities);
トークンの取得
POST /oauth/token
OAuth 2.0 Client Credentials(RFC 6749 §4.4)です。資格情報は HTTP Basic
(base64(client_id:client_secret))で送るか、本文に client_id / client_secret を入れます。
標準的な OAuth ライブラリ(requests-oauthlib、openid-client など)でもそのまま取得できます。
POST /oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=judge
{"access_token": "<JWT>", "token_type": "Bearer", "expires_in": 3600, "scope": "judge"}
トークンは 1 時間有効です。リクエストのたびに取り直さず、expires_in を見て期限の少し前まで使い回してください。
クライアントをダッシュボードで失効させると、発行済みのトークンも即座に使えなくなります。
エラーは RFC 6749 §5.2 の形式です(このエンドポイントだけ他と形式が違います)。
{"error": "invalid_client", "error_description": "…"}
| error | HTTP | 意味 |
|---|---|---|
invalid_client | 401 | client_id / client_secret が違う、またはクライアントが失効している |
invalid_request | 400 | 必須パラメータが無い・形式が違う |
unsupported_grant_type | 400 | grant_type が client_credentials 以外 |
invalid_scope | 400 | scope が judge 以外 |
TypeSafe AI の SDK で使う
GyoJy は、TypeSafe AI, Inc. が提供する判定 API と同じリクエスト・レスポンスの形の POST /v1/systemone・GET /v1/models も受け付けます。
同社の公式 SDK(Python の typesafe-sdk、JavaScript の @typesafe-ai/sdk)は、接続先と API キーを差し替えるだけで GyoJy に向けて使えます。
API キーには、ダッシュボードで発行した Secret(gvj_cs_…)をそのまま使います(OAuth のトークン取得は不要です。Secret は長期の秘密なので、サーバ側でだけ使ってください)。
export TYPESAFE_BASE_URL=https://gyojy.hakadoru.ai/api
export TYPESAFE_API_KEY=gvj_cs_xxxxxxxx
# pip install typesafe-sdk
from typesafe_sdk import TypeSafeClient
client = TypeSafeClient() # TYPESAFE_BASE_URL / TYPESAFE_API_KEY を読む
r = client.system_one(
state={"subject": "二重請求", "message": "同じ注文で 2 回引き落とされました"},
model="gyojy-latest",
questions={
"billing": {"type": "noul", "instructions": "これは請求に関する問い合わせか?"},
"urgency": {"type": "score", "instructions": "緊急度は?", "criteria": ["後でよい", "今週中", "今日中"]},
},
)
print(r.answers["billing"].noul, r.answers["urgency"].score)
- 質問の種類
choice/noul/scoreとconfidenceの定義は、同社の公開仕様に合わせています。SDK の既定のモデル名もそのまま受け付けます - 違い: 画像を読める(
images、GyoJy 独自)、choiceの選択肢は 26 個まで、coverageが付く、応答のmodelはgyojy-1 - 推論サーバが停止中だと
503(overloaded_error、Retry-After: 90)が返ります。SDK の既定の再試行(合計 30 秒)では起動を待ちきれないので、先にPOST /v1/warmupで起こしておくか、再試行の設定を延ばしてください - エラーはこのパスだけ
{"detail": …}の形(検証エラーは 422)で返ります。残高不足は 402(error_type: insufficient_credits)
Jev および System One は TypeSafe AI, Inc. の商標または登録商標である可能性があります。GyoJy はアサラボが独自に運営するサービスで、TypeSafe AI, Inc. とは提携・承認・後援の関係にありません。 SDK は同社が公開しているものを、互換のために案内しています。
POST /v1/judge
POST /v1/judge
文章と画像を材料に、各質問について確率を返します。形式は /v1/systemone と同じで、images を足せます。エラーは GyoJy 形式({"error": …})です。
リクエスト
{
"state": "今日は半袖でも心地良い気温でした。",
"images": ["data:image/jpeg;base64,/9j/4AAQ..."],
"questions": {
"temperature": {
"type": "choice",
"instructions": "気温として最も適切なのは?",
"criteria": {"18度": null, "22度": null, "26度": "暑い日"}
}
}
}
| フィールド | 型 | 説明 |
|---|---|---|
state | string / object / array | 判断の材料。オブジェクトや配列は整形した JSON として読みます(16,000 文字まで、JSON にした長さで数える)。state と images の少なくとも一方が必要 |
images | string[] | 0〜4 枚。data: URL のみ(png / jpeg / webp / gif)。URL やパスは指定できません |
questions | object | 質問 ID → 質問。1〜10 個。質問 ID は応答の answers のキーになります |
questions.*.type | "choice" / "noul" / "score" | 省略時は choice(下の表) |
questions.*.instructions | string / object / array / null | 問い。2,000 文字まで。オブジェクトに例文を入れると、少数例の提示(few-shot)の代わりになります |
questions.*.criteria | 種類による | 下の表 |
model | string | 省略可。gyojy-1(別名 gyojy-latest) |
| type | 用途 | criteria | 答え |
|---|---|---|---|
choice | 選択肢から 1 つ選ぶ | 選択肢 → 説明(不要なら null)。2〜26 個 | choice・probabilities・confidence |
noul | はい / いいえ、文の真偽 | 省略可。{"true": "はいに当たるもの", "false": "いいえに当たるもの"} | noul(はいの確率。confidence は無い) |
score | 段階で評価する | 段階の説明の配列(1〜10 個、先頭が 0) | score(段階の期待値)・legend・probabilities(キー "0" "1" …)・confidence |
応答
{
"model": "gyojy-1",
"served_by": "gvj",
"answers": {
"temperature": {
"type": "choice",
"choice": "22度",
"probabilities": {"18度": 0.0001, "22度": 0.8599, "26度": 0.1401},
"confidence": 0.7897,
"coverage": 0.9981
}
},
"usage": {"input_tokens": 1180, "output_tokens": 1, "elapsed_ms": 412, "credits": 170}
}
| フィールド | 説明 |
|---|---|
choice | 最も確率の高い選択肢 |
probabilities | 選択肢ごとの確率(合計 1 に正規化済み) |
confidence | 答えの確からしさ(0〜1)。本家と同じ定義で、choice は (最大確率 − 1/n) / (1 − 1/n)、score は最頻の段階からの散らばりの少なさ。しきい値で人の確認に回す判定に使えます |
coverage | 正規化前に、選択肢の記号へ落ちていた確率の合計。低いほどモデルが選択肢以外を答えたがっている |
usage.credits | このリクエストで消費したクレジット |
費用: 推論(質問 1 つにつき 1 回)・画像(1 枚につき、読む 1 回と受け取り・前処理 1 回)・state の文字数(1,000 文字あたり 0.4012 円相当)の単価を足し、リクエストごとに 1 回だけクレジット(1 クレジット = 0.01 円、税別)へ切り上げます。画像と state はリクエスト内で使い回すため、質問を増やしても増えるのは推論の分だけです。上の例(画像 1 枚・17 文字・質問 1 つ)は 170 クレジットです(料金)。
POST /v1/compare
POST /v1/compare
2 枚の画像、または 2 つの文章のどちらが基準に合うかを判定します。順番を入れ替えて 2 回(A→B、B→A)聞き、
答えが食い違えば undecided を返すので、先に見せた方が有利になる位置バイアスを打ち消せます。
images と texts はどちらか一方だけを渡します。
リクエスト(画像)
{
"images": {"A": "data:image/png;base64,...", "B": "data:image/png;base64,..."},
"criterion": "判断基準: より明るい画像が勝ち",
"tie": false
}
| フィールド | 型 | 説明 |
|---|---|---|
images.A / images.B | string | 比較する 2 枚(data: URL)。texts と同時には指定できない |
texts.A / texts.B | string | 比較する 2 つの文章。各 1〜2,000 文字。images と同時には指定できない |
criterion | string | 判断基準。1〜2,000 文字 |
tie | boolean | true なら「引き分け」も選択肢に入れる |
force_decision | boolean | 既定 false。true なら、2 回の答えが食い違ったときも undecided にせず、平均の確率で決める(平均が完全に同じなら A) |
margin | number | 0 以上 0.5 未満、既定 0。平均の確率が 0.5 ± margin に入る(A と B の差が 2 × margin 未満の)ときは undecided にする |
リクエスト(文章)
文章どうしも同じ形で比べられます。言い回しの候補から、基準に合う方を選ぶといった用途に使えます。
{
"texts": {
"A": "ご注文いただいた商品は、明日の午前中にお届けします。",
"B": "ご注文の品は明日午前着の予定です。お届け時間の変更はマイページから承ります。"
},
"criterion": "判断基準: お客様が次にとれる行動が分かる案内文が勝ち"
}
curl -sS -X POST "https://gyojy.hakadoru.ai/api/v1/compare" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"texts": {"A": "…", "B": "…"}, "criterion": "判断基準: …"}'
応答
{
"verdict": "A",
"consistent": true,
"probabilities": {"A": 0.935, "B": 0.065},
"runs": [
{"order": ["A", "B"], "probabilities": {"A": 0.97, "B": 0.03}},
{"order": ["B", "A"], "probabilities": {"A": 0.90, "B": 0.10}}
],
"usage": {"input_tokens": 4120, "output_tokens": 0, "elapsed_ms": 1310, "credits": 578}
}
verdict は A / B / tie(tie: true のときのみ)/ undecided(2 回の答えが食い違った)。
runs に各回の結果が入ります。応答の形は画像でも文章でも同じです。
判定の決まり方(force_decision と margin)
既定では、undecided は差の大きさではなく、2 回の答えが一致したかどうかで決まります。
僅差でも 2 回とも同じ側を選べば勝敗がつき、平均では大差でも片方の回でひっくり返れば undecided です。
差の大きさは probabilities(2 回の平均)で見られます。用途に合わせて、次の 2 つで決まり方を変えられます。
| 2 回の A の確率 | 平均 | 既定 | force_decision | margin: 0.05 |
|---|---|---|---|---|
| 0.55 と 0.52(僅差、どちらも A) | 0.535 | A | A | undecided |
| 0.97 と 0.90(大差、どちらも A) | 0.935 | A | A | A |
| 0.95 と 0.45(1 回目は A、2 回目は B) | 0.70 | undecided | A | undecided |
| 0.90 と 0.10(どちらも先に見せた方を選んだ) | 0.50 | undecided | A | undecided |
force_decision: trueは、必ずどちらかに決めたいとき(候補の自動選択など)に使います。食い違ったかどうかはconsistent: falseで分かりますmarginは、僅差を「差なし」として扱いたいとき(人の確認に回すなど)に使います。force_decisionと両方指定すると、食い違いを平均で決めたあとにmarginを当てるので、僅差ならundecidedですtie: trueで「引き分け」が選ばれたときは、marginは関係しません- どちらを指定しても費用は変わりません
費用は、画像なら 1 回 578 クレジット(5.78 円、税別。2 回推論 × 画像 2 枚を読む + 2 枚の受け取りと前処理)。 文章なら 2 回推論と、各回で 2 つの文章を読む文字数分(2 ×(A の文字数 + B の文字数))です。上の文章の例(26 文字と 38 文字)は 67 クレジット(0.67 円)です。
POST /v1/rank
POST /v1/rank
たくさんの候補(画像だけ、または文章だけ)を 2 つずつ比べ、相対的な順位とレーティングを返します。
画像も文章も、同じ API・同じ応答の形で扱えます。
候補を円環に並べ、2〜5 件なら総当り、6 件以上なら隣どうしと、なるべく遠い相手を比べます(n 件で 2n 戦、各候補 4 戦)。
各対戦は /v1/compare と同じく順番を入れ替えて 2 回聞き、勝ち負けではなく確率のまま集計して、Bradley–Terry モデルで採点します(Elo 尺度、平均 1500)。
リクエスト(画像)
{
"criterion": "判断基準: より食欲をそそる写真が勝ち",
"items": [
{"id": "a", "image": "data:image/jpeg;base64,..."},
{"id": "b", "image": "data:image/jpeg;base64,..."},
{"id": "c", "image": "data:image/jpeg;base64,..."}
]
}
リクエスト(文章)
文章は {"text": "..."} で渡します。キャッチコピーや説明文の候補を、基準に沿って並べ替えるといった用途に使えます。
{
"criterion": "判断基準: 読んだ人が食べたくなる商品説明が勝ち",
"items": [
{"id": "a", "text": "季節の果実を使った、甘さ控えめのタルトです。"},
{"id": "b", "text": "濃厚なチーズと、ほろ苦いキャラメルの二層仕立て。"},
{"id": "c", "text": "朝摘みの苺をたっぷりのせた、春だけのショートケーキ。"},
{"id": "d", "text": "定番のガトーショコラ。"}
]
}
この例(4 件、総当り 6 戦 = 12 回推論)は 390 クレジット(3.90 円、税別)です。
- 画像は 2〜10 枚、文章は 2〜30 件(各 2,000 文字まで)。画像と文章は 1 回のリクエストに混ぜられません
idは省略可(省略時は 0 から始まる番号)。seed(任意の文字列)で円環の並びを変えられます。省略すると内容から決まるので、同じ候補なら同じ組み合わせになります
応答
{"ranking": [{"id": "b", "rank": 1, "rating": 1612.4, "win_rate": 0.8123, "matches": 2},
{"id": "a", "rank": 2, "rating": 1488.0, "win_rate": 0.4561, "matches": 2},
{"id": "c", "rank": 3, "rating": 1399.6, "win_rate": 0.2316, "matches": 2}],
"matches": [{"a": "a", "b": "b", "p_a": 0.1877, "consistent": true}, ...],
"ring": ["c", "a", "b"],
"usage": {"inferences": 6, "credits": 1682, ...}}
費用は内訳(推論の回数・画像を読む回数・画像の受け取りと前処理・文章の文字数)の合計です。画像は最初に 1 回だけ前処理して以後の対戦で使い回すため、
同じ対戦を /v1/compare で 1 つずつ呼ぶより安くなります。詳しくは料金。1 対戦でも失敗したときは全額返金します。
GET /v1/balance
GET /v1/balance
残高と、残高のお知らせ(メール)の設定を返します。費用はかかりません。
curl -sS "https://gyojy.hakadoru.ai/api/v1/balance" -H "Authorization: Bearer $TOKEN"
{"balance": 12345,
"expirations": [{"expires_at": "2026-09-30T14:59:59.999Z", "credits": 12345}],
"low_balance_alert": {"enabled": true, "threshold_credits": 10000}}
expirations は有効期限ごとの残りです(早い順)。クレジットは購入月を 1 か月目として 5 か月目の末日 23:59:59(日本時間)に失効します。
low_balance_alert は、残高が threshold_credits を下回ったときに登録メールアドレスへお知らせを 1 通送る設定です(変更はダッシュボードで行います)。
GET /v1/status・POST /v1/warmup
推論サーバ(GPU)は、しばらくリクエストが無いと停止します。停止中に推論 API を呼ぶと起動が始まり、
503 warming_up(Retry-After: 90)が返ります。このときは課金されません。1〜2 分後に同じリクエストを送り直してください。
まとめて処理する前に POST /v1/warmup で起こしておくと確実です。どちらも費用はかかりません。
curl -sS -X POST "https://gyojy.hakadoru.ai/api/v1/warmup" -H "Authorization: Bearer $TOKEN" # 202 {"worker":"starting","retry_after":90} / 200 {"worker":"ready"}
curl -sS "https://gyojy.hakadoru.ai/api/v1/status" -H "Authorization: Bearer $TOKEN" # {"worker":"ready"} / {"worker":"stopped_or_starting"}
エラー
/oauth/token 以外のエラーは次の形式です。code は英語の固定値、message は日本語の説明です。
プログラムでは code で分岐してください。
{"error": {"code": "insufficient_credits", "message": "…"}}
| code | HTTP | 意味 | 課金 |
|---|---|---|---|
invalid_token | 401 | Bearer が無い・不正・期限切れ、またはクライアントが失効している。トークンを取り直す | なし |
invalid_request | 400 | 形式・上限の違反(message に箇所) | なし |
insufficient_credits | 402 | 残高不足。必要額 required と残高 balance が併記される | なし |
payload_too_large | 413 | 本文が 5MB を超えた。画像を縮小する | なし |
rate_limited | 429 | 会員あたり 10 リクエスト/秒を超えた。間隔を空けて再試行 | なし |
no_label | 422 | 選択肢の記号がモデルの上位 20 候補に現れなかった。問い方を見直す | 返金 |
warming_up | 503 | 推論サーバが停止していたので起動を始めた。Retry-After(90)秒後に再試行 | 課金しない |
busy | 503 | 推論サーバが混雑している。Retry-After 秒後に再試行 | 返金 |
upstream_error | 502 | 推論サーバの障害。時間を置いて再試行 | 返金 |
上限
| 項目 | 上限 |
|---|---|
| リクエスト本文 | 5MB |
| リクエスト頻度 | 会員あたり 10 リクエスト/秒 |
質問数(/v1/judge) | 1〜10 |
| 選択肢の数 | 2〜26(キーは 1〜200 文字) |
instructions | 2,000 文字 |
state | 16,000 文字 |
画像(/v1/judge) | 0〜4 枚。各 data: URL(png / jpeg / webp / gif)、デコード後 4MB・4,000 万画素まで |
criterion(/v1/compare) | 1〜2,000 文字 |
| アクセストークンの有効期限 | 1 時間 |
| 有効な API クライアント | 1 会員あたり 5 個 |
使い方のコツ
画像は長辺 1024px に縮小してから送る
サーバ側でも長辺 1024px に縮小してから推論しますが、API の入口ではアップロードが 512KB 単位で計量されるため、 大きな画像をそのまま送ると転送が遅くなり、5MB の上限にも当たりやすくなります。 送る前に縮小すれば応答が速くなります(クレジットは画像の枚数で決まり、サイズでは変わりません)。
# pip install pillow
import base64
import io
from PIL import Image, ImageOps
def shrink_data_url(path: str, long_edge: int = 1024) -> str:
"""EXIF の回転を反映し、長辺 long_edge px に縮小した JPEG の data: URL を返す。"""
im = ImageOps.exif_transpose(Image.open(path))
im.thumbnail((long_edge, long_edge)) # 小さい画像は拡大しない
if im.mode != "RGB":
# 透過は白背景に合成する
rgba = im.convert("RGBA")
bg = Image.new("RGB", rgba.size, (255, 255, 255))
bg.paste(rgba, mask=rgba.getchannel("A"))
im = bg
buf = io.BytesIO()
im.save(buf, "JPEG", quality=90)
return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode("ascii")
coverage が 0.5 を下回ったら問い方を変える
coverage は、モデルが選択肢の記号に割り当てた確率の合計です。0.5 未満なら、モデルは選択肢以外の答えを出したがっています。
probabilities は正規化済みなのでそれらしく見えますが、信用できません。
選択肢に「どれでもない」を足す、instructions を具体的にする、criteria に補足説明を書く、などで言い換えてください。
その他
- アクセストークンは使い回す(毎回
/oauth/tokenを呼ばない) - 同じ画像について複数のことを聞くときは、1 リクエストに質問をまとめる(画像の費用が 1 回で済む)
busy(503)はRetry-After、rate_limited(429)は少し間隔を空けて再試行する- しきい値判定は
probabilitiesとconfidenceで行い、低いものだけ人の確認に回す client_secretはソースコードに書かず、環境変数やシークレットストアに置く。漏れたらダッシュボードで失効させて発行し直す