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": "…"}
errorHTTP意味
invalid_client401client_id / client_secret が違う、またはクライアントが失効している
invalid_request400必須パラメータが無い・形式が違う
unsupported_grant_type400grant_type が client_credentials 以外
invalid_scope400scope が 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度": "暑い日"}
    }
  }
}
フィールド型説明
statestring / object / array判断の材料。オブジェクトや配列は整形した JSON として読みます(16,000 文字まで、JSON にした長さで数える)。state と images の少なくとも一方が必要
imagesstring[]0〜4 枚。data: URL のみ(png / jpeg / webp / gif)。URL やパスは指定できません
questionsobject質問 ID → 質問。1〜10 個。質問 ID は応答の answers のキーになります
questions.*.type"choice" / "noul" / "score"省略時は choice(下の表)
questions.*.instructionsstring / object / array / null問い。2,000 文字まで。オブジェクトに例文を入れると、少数例の提示(few-shot)の代わりになります
questions.*.criteria種類による下の表
modelstring省略可。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.Bstring比較する 2 枚(data: URL)。texts と同時には指定できない
texts.A / texts.Bstring比較する 2 つの文章。各 1〜2,000 文字。images と同時には指定できない
criterionstring判断基準。1〜2,000 文字
tiebooleantrue なら「引き分け」も選択肢に入れる
force_decisionboolean既定 false。true なら、2 回の答えが食い違ったときも undecided にせず、平均の確率で決める(平均が完全に同じなら A)
marginnumber0 以上 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_decisionmargin: 0.05
0.55 と 0.52(僅差、どちらも A)0.535AAundecided
0.97 と 0.90(大差、どちらも A)0.935AAA
0.95 と 0.45(1 回目は A、2 回目は B)0.70undecidedAundecided
0.90 と 0.10(どちらも先に見せた方を選んだ)0.50undecidedAundecided
  • 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": "…"}}
codeHTTP意味課金
invalid_token401Bearer が無い・不正・期限切れ、またはクライアントが失効している。トークンを取り直すなし
invalid_request400形式・上限の違反(message に箇所)なし
insufficient_credits402残高不足。必要額 required と残高 balance が併記されるなし
payload_too_large413本文が 5MB を超えた。画像を縮小するなし
rate_limited429会員あたり 10 リクエスト/秒を超えた。間隔を空けて再試行なし
no_label422選択肢の記号がモデルの上位 20 候補に現れなかった。問い方を見直す返金
warming_up503推論サーバが停止していたので起動を始めた。Retry-After(90)秒後に再試行課金しない
busy503推論サーバが混雑している。Retry-After 秒後に再試行返金
upstream_error502推論サーバの障害。時間を置いて再試行返金

上限

項目上限
リクエスト本文5MB
リクエスト頻度会員あたり 10 リクエスト/秒
質問数(/v1/judge)1〜10
選択肢の数2〜26(キーは 1〜200 文字)
instructions2,000 文字
state16,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 はソースコードに書かず、環境変数やシークレットストアに置く。漏れたらダッシュボードで失効させて発行し直す