最近、SNS などを見ていると、
- 「gpt-5-nano は安いけど遅い」
- 「gpt-5.4-nano は安くて速い」
という評価を見かける気がします。
実際、デフォルト設定のまま使うなら、かなりその印象に近いです。
しかし個人的には、reasoning_effort を調整すると、もう少し考える余地があると思っています。
reasoning_effort: "minimal" を使うと印象が変わる
特に gpt-5-nano や gpt-5-mini は、
reasoning_effort: "minimal"
を指定すると、かなり高速になります。
体感では、
gpt-5-nano (minimal)gpt-5-mini (minimal)
は、普通に gpt-5.4-nano を使うより速いケースがかなり多いです。
また、精度面についても、
「gpt-5-nano minimal は確かに弱い」
という印象が明確にあるものの、そこまで極端には悪くありません。
特に、
- ある程度出力内容が決まっている生成
- 軽い文章生成
- 会話補助
- チャット返信
- 簡単な分類
- 軽量エージェント
あたりでは、「料金差を考えると十分では?」と感じる場面が多いです。
gpt-5-mini minimal はかなり強い
さらに個人的には、
gpt-5-mini + reasoning_effort: "minimal"
はかなり優秀だと感じています。
体感では、
- 速度はかなり速い
- 出力品質は高い
- gpt-5.4-nano より生成内容が良いケースが多い
という印象です。
もちろん用途次第ですが、少なくとも
「料金差ほど gpt-5.4-nano を優遇する必要はない」
とは感じています。
実際の料金感
例えば、
とすると、1リクエスト当たりの料金はおおよそ以下になります。
| モデル | 1リクエスト |
|---|---|
| gpt-5-nano | 0.0316 円 |
| gpt-5.4-nano | 0.1094 円 |
| gpt-5-mini | 0.1579 円 |
これを 1か月 3万リクエスト(1日1000リクエスト程度)で計算すると、
| モデル | 月額 |
|---|---|
| gpt-5-nano | 948 円 |
| gpt-5.4-nano | 3,281 円 |
| gpt-5-mini | 4,739 円 |
になります。
この差はかなり大きいです。
特に API を大量に叩く用途では、
- 「少し精度を落としてでも安くする」
- 「速度を優先する」
- 「回転率を重視する」
という考え方が重要になるため、単純に「精度だけ」で比較しにくいと思っています。
料金比較はこのサイトが便利
下記サイトでは、
- Input
- Output
- ドル円
を入力すると、各モデルの料金をすぐ比較できます。
個人的にもかなり便利だと思っています。
URL をコピーボタンで、現在の入力内容でアクセスできるようになる URL をコピーできます。
ご自身の会社に合っていると思う条件を指定した上で、上司や同僚に API 料金情報を共有できます。
flex 出力もかなりおすすめ
また、service_tier: "flex" の利用もおすすめです。
一般的には、
- 優先度を下げる代わりに半額
- その代わり遅い
という説明を見かけます。
ただ、実際に使ってみると、
「思ったほど遅くない」
という印象があります。
むしろ、
- 大量リクエスト
- バックグラウンド処理
- 非同期生成
- 多少待てる用途
では、かなり相性が良いです。
実装例
例えば、Cloudflare Workers 側でこんな感じにすると使いやすいです。
コード抜粋
const PUBLIC_TIME_KEY = "T1cu5SKPk17oh9LYRZlNn4jhpDju7BTyttwdsAPjd8cwewyN0XkSTqIq2gefg7ZPg3";
const body = await request.json();
const prompt = body?.prompt;
const modelMode = body?.modelMode;
const timeToken = body?.timeToken;
const timeCheck = await verifyEncryptedDateTimeToken(
timeToken,
PUBLIC_TIME_KEY
);
if (!timeCheck.ok) {
return json(
{ error: "Invalid request." },
400
);
}
let model = "gpt-5-mini"; //使いたいモデル
if(modelMode != "default") {
model = "gpt-5-nano";
}
// ここから model / messages を組み立てる
let upstream = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${env.MyKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
messages,
// gpt-5 系は temperature / max_tokens がエラー要因になりやすいので送らない
stream: true,
reasoning_effort,
service_tier: "flex",
}),
});
if (!upstream.ok && upstream.status === 429) {
upstream = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${env.MyKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
messages,
// gpt-5 系は temperature / max_tokens がエラー要因になりやすいので送らない
stream: true,
reasoning_effort,
}),
});
}
if (!upstream.ok) {
// 外部には詳細を出さない
// upstream.status / upstream.statusText / response body は返さない
console.warn("Upstream request failed", {
status: upstream.status,
// 必要なら requestId など、個人情報・API応答本文を含まない情報だけ
});
return json(
{ error: "Request failed. Please try again later." },
502
);
}
// バイナリのまま再ストリーム
// レスポンス本文はデコード・ログ出力・加工しない
const stream = new ReadableStream({
start(controller) {
const reader = upstream.body.getReader();
(async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Uint8Array のまま転送する
controller.enqueue(value);
}
controller.close();
} catch (err) {
// エラー詳細をクライアントに返さない
console.error("Stream forwarding failed");
try {
controller.error(new Error("Stream failed"));
} catch {}
}
})();
}
});
return new Response(stream, {
status: 200,
headers: corsSSEHeaders(),
});
/* 以下メソッド群 */
async function verifyEncryptedDateTimeToken(timeToken, base62Key) {
try {
if (typeof timeToken !== "string" || timeToken.length < 32) {
return { ok: false };
}
const decryptedText = await decryptTextWithBase62Key(timeToken, base62Key);
const payload = JSON.parse(decryptedText);
const dateTime = payload?.dateTime;
if (typeof dateTime !== "string") {
return { ok: false };
}
const createdAt = Date.parse(dateTime);
if (!Number.isFinite(createdAt)) {
return { ok: false };
}
const now = Date.now();
const diffMs = Math.abs(now - createdAt);
if (diffMs > 5000) {
return { ok: false };
}
return { ok: true };
} catch {
return { ok: false };
}
}
async function decryptTextWithBase62Key(token, base62Key) {
const merged = base64UrlDecode(token);
if (merged.length <= 12) {
throw new Error("Invalid token");
}
const iv = merged.slice(0, 12);
const encrypted = merged.slice(12);
const key = await deriveAesKeyForDecrypt(base62Key);
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv
},
key,
encrypted
);
return new TextDecoder().decode(decrypted);
}
async function deriveAesKeyForDecrypt(base62Key) {
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(base62Key)
);
return crypto.subtle.importKey(
"raw",
hash,
{ name: "AES-GCM" },
false,
["decrypt"]
);
}
function base64UrlDecode(text) {
const base64 = text
.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(Math.ceil(text.length / 4) * 4, "=");
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
// 公開用サンプルでは、実際のモデル名・接続先URL・内部識別子は固定で露出させない
const PUBLIC_TIME_KEY = "T1cu5SKPk17oh9LYRZlNn4jhpDju7BTyttwdsAPjd8cwewyN0XkSTqIq2gefg7ZPg3";
const modelMode = "default";
const timeToken = await createEncryptedDateTimeToken(PUBLIC_TIME_KEY);
const res = await fetch("https://example.com/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt,
modelMode,
timeToken
})
});
if (!res.ok) {
// 公開画面には詳細なステータス・内部エラー文を出さない
// 必要な場合のみ、開発環境で最小限ログに残す
if (typeof DEBUG_MODE !== "undefined" && DEBUG_MODE) {
console.warn("Request failed", {
status: res.status
});
}
throw new Error("生成に失敗しました。時間をおいて再度お試しください。");
}
// 受信した文字列を蓄積する配列
const chunkList = [];
// すでに確定済みの末尾位置
let confirmedLen = 0;
// 生成中のプレビュー表示用
let currentButton = null;
// 区切り文字
const REPLY_SEPARATOR = "|||";
// 受信済みテキストを処理して、確定済み返信とプレビューを更新する
function processChunkList() {
const textSoFar = chunkList.join("");
let searchPos = confirmedLen;
while (true) {
const idx = textSoFar.indexOf(REPLY_SEPARATOR, searchPos);
if (idx === -1) break;
const oneReply = textSoFar.slice(confirmedLen, idx).trim();
confirmedLen = idx + REPLY_SEPARATOR.length;
searchPos = confirmedLen;
if (oneReply) {
currentButton = createOrUpdateReply(currentButton, oneReply);
currentButton = null;
}
}
const preview = textSoFar.slice(confirmedLen).trim();
if (preview) {
currentButton = createOrUpdateReply(currentButton, preview);
}
}
try {
await readOpenAIStream(res, (json) => {
const content = json?.choices?.[0]?.delta?.content;
if (!content) return;
chunkList.push(content);
processChunkList();
});
status.textContent = "生成完了。クリックでコピーできます。";
} catch (err) {
if (typeof DEBUG_MODE !== "undefined" && DEBUG_MODE) {
console.error("Stream processing failed");
}
status.textContent = "生成中にエラーが発生しました。時間をおいて再度お試しください。";
}
/* 以下メソッド群 */
function createOrUpdateReply(button, text) {
const safeText = String(text || "").trim();
if (!safeText) return button;
// 既存ボタンがなければ作成
if (!button) {
button = document.createElement("button");
button.type = "button";
button.className = "reply-button";
button.dataset.replyText = safeText;
button.addEventListener("click", async () => {
const replyText = button.dataset.replyText || button.textContent || "";
try {
await navigator.clipboard.writeText(replyText);
status.textContent = "コピーしました。";
} catch {
status.textContent = "コピーに失敗しました。";
}
// 手動修正欄がある場合だけ反映
const editArea = document.getElementById("manualEdit");
if (editArea) {
editArea.value = replyText;
}
});
const replyList = document.getElementById("replyList");
if (replyList) {
replyList.appendChild(button);
}
}
// 表示とコピー用データを更新
button.textContent = safeText;
button.dataset.replyText = safeText;
return button;
}
async function createEncryptedDateTimeToken(base62Key) {
const dateTime = getJapanDateTimeString();
const payload = JSON.stringify({
dateTime,
nonce: crypto.randomUUID()
});
return encryptTextWithBase62Key(payload, base62Key);
}
function getJapanDateTimeString() {
const parts = new Intl.DateTimeFormat("sv-SE", {
timeZone: "Asia/Tokyo",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}).format(new Date());
return parts.replace(" ", "T") + "+09:00";
}
async function encryptTextWithBase62Key(text, base62Key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveAesKey(base62Key);
const encrypted = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
key,
new TextEncoder().encode(text)
);
const merged = new Uint8Array(iv.length + encrypted.byteLength);
merged.set(iv, 0);
merged.set(new Uint8Array(encrypted), iv.length);
return base64UrlEncode(merged);
}
async function deriveAesKey(base62Key) {
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(base62Key)
);
return crypto.subtle.importKey(
"raw",
hash,
{ name: "AES-GCM" },
false,
["encrypt"]
);
}
function base64UrlEncode(bytes) {
let binary = "";
for (const b of bytes) {
binary += String.fromCharCode(b);
}
return btoa(binary)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
}
flex で試し、429 が返ったら通常 Tier にフォールバックする形です。
こうしておくと、
- 普段は安く運用
- 混雑時だけ通常 Tier
という構成にできる可能性があります。
Cloudflare Workers はかなり相性が良い
個人的には、この用途なら Cloudflare Workers がかなりおすすめです。
理由としては、
- 軽い
- デプロイが簡単
- SSE ストリーミングと相性が良い
- fetch ベースで実装しやすい
- 無料枠でも試しやすい
あたりが大きいです。
OpenAI API の中継サーバーとしても非常に扱いやすいと思います。
まとめ
個人的な感覚としては、
- 「gpt-5-nano は遅い」は default 設定前提なら確かにそう
- ただし
reasoning_effort: "minimal"でかなり印象が変わる - コスト差を考えると十分実用的
-
gpt-5-mini minimalは特に強い -
flexも思ったより実用的
という感じです。
結局のところ、
- 何を作るか
- どこまで品質が必要か
- レスポンス速度をどこまで重視するか
- 月間リクエスト数がどれくらいか
によって最適解はかなり変わると思っています。
gpt-5-mini + reasoning_effort: "minimal"
の方が早いから、そちらにしようと思ったのならば、
gpt-5.4-nano + reasoning_effort: "none"
も試してみる等、色々な条件で試してから結論を出すのが良いと思われます。
下記のようなサイトを作って、試すのが良いと思われます。使うならばダウンロードして、安全性を確認してから使う事を推奨します。
安全性の確認は、網羅性がなく確実ではありませんが、ChatGPT に添付して、添付サイトに、外部送信など悪意のあるコードが含まれていないか聞くのも選択肢の一つです。
結論としては、少なくとも、
「gpt-5.4-nano 一択」
というほど単純ではない、というのが今の個人的な感想です。