0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ChatGPT】gpt-5.4-nano の「早くて安い」という評価について思う事

0
Last updated at Posted at 2026-05-08

最近、SNS などを見ていると、

  • 「gpt-5-nano は安いけど遅い」
  • 「gpt-5.4-nano は安くて速い」

という評価を見かける気がします。
実際、デフォルト設定のまま使うなら、かなりその印象に近いです。

しかし個人的には、reasoning_effort を調整すると、もう少し考える余地があると思っています。

reasoning_effort: "minimal" を使うと印象が変わる

特に gpt-5-nanogpt-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 一択」

というほど単純ではない、というのが今の個人的な感想です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?