💸

Vertex AI・Gemini APIの料金最適化 実践ガイド

に公開

Google Cloud Vertex AI Platformは、Googleの最先端の生成AIモデル、特にGeminiを統合的に利用できる強力なプラットフォームです。APIの利用自体はシンプルですが、モデルごとに設定できる内容やデフォルト設定が異なるため、適切な設定を行わないと想定以上の課金が発生することがあります。

本記事では、Generative AI on Vertex AI (Gemini API)について、実務での経験を踏まえた低コスト運用のための観点と実装例を紹介します。適切な利用により、同じ処理でも50%〜90%のコスト削減が可能なケースもあります。

そもそも入出力トークン数は妥当か?

最も基本的なコスト削減策は、トークン数そのものを減らすことです。例えば、入力の言い回しをより簡潔にする、出力は商品名ではなくindexを返すようにする、といった些細な工夫で大幅に課金額を減らすことができる場合があります。

特に現在のGeminiモデル群は出力に入力の数倍の単価が設定されている[1]ため、後者の出力トークン数の削減に関する施策は効果が大きくなる傾向があります。
例えばGemini 2.5 Flashでは100万トークンあたり入力が$0.30なのに対し、出力は$2.50と8倍以上の価格が設定されています(2025年10月時点)。他の類似サービスに比べるとGeminiは廉価ではありますが、この価格差は非常に大きいため注意して設計する必要があります。

各Geminiモデルの入出力単価の抜粋 / いずれも100万トークンあたりの基本価格[1:1] (2025年10月時点)

入力単価 出力単価 入出力単価比
Gemini 2.5 Pro $1.25 $10 8.0倍
Gemini 2.5 Flash $0.3 $2.5 8.3倍
Gemini 2.5 Flash Lite $0.1 $0.4 4.0倍

入力は処理をバッチ化(複数まとめる)したり、文章よりも箇条書きで指示を与えたり、重複する指示を削除するなどの工夫が効果的です。
特にバッチ化は効果的で、例えば5回分の処理を1回のリクエストに詰め込み、JSONで構造化して受け取って実装側で展開すれば、単純計算で1/5程度まで入力トークン数を圧縮することができます。ただし、これによって露骨にパフォーマンスが低下しないよう、調整する作業が必要になります。

出力については、例えば以下のような出力をさせて商品を選択させるタスクを課すと353トークンを消費する一方で、indexだけを出力させるようにすれば23トークンまで圧縮できるため、同じ予算で15倍処理することができます。

適当にGeminiで生成した商品名たち.json
[
   {
      "title":"【堂々1位獲得】公式サイト限定★創業20周年!!豪華感謝祭★とろける生チョコレート 50個入り 期間限定 厳選カカオ使用 人気 ギフト高評価 送料無料 贈答 プレゼント 2025 CP-20thANNIV"
   },
   {
      "title":"【殿堂入り】今だけ限定★特別企画★幻の高級和牛モモステーキ 約400g 2枚 極上とろける旨み 期間限定 人気 お歳暮 贈答品 送料無料 産地直送 2025 GIFT-WAGYU"
   },
   {
      "title":"【金賞】WEB限定★5周年記念!!大感謝セール★濃厚チーズケーキ 1ホール 絶品とろける口どけ 期間限定 人気 スイーツ ギフト高評価 お取り寄せ 送料無料 2025 CAKE-CHEESE"
   },
   {
      "title":"【販売数No.1】限定★日頃の感謝を込めて★とれたて新鮮野菜セット 約3kg 10種類 旬の味覚満載 期間限定 人気 産地直送 高評価 送料無料 健康 ギフト 2025 VEGE-FRESH"
   },
   {
      "title":"【お客様満足度98%】ブランド公式サイト限定★特別感謝キャンペーン★極上はちみつ紅茶 ティーバッグ30個入 優雅な香り 期間限定 人気 リラックスタイム ギフト高評価 送料無料 2025 TEA-HONEY"
   }
]
トークン効率の良いレスポンス例.json
[{"index":3},{"index":0},{"index":2},{"index":5},{"index":1}]

思考予算(thinking budget)は適切か?

トークン数を最適化しても、見落としがちなのが「思考予算」による課金です。この設定を見逃すと、出力トークン数を削減したにも関わらず、想定外の課金が発生する可能性があります。

思考予算(thinking budget)とは、Geminiモデルが複雑なタスクを実行する際に、ユーザーへの応答を生成する前に内部的な推論や計画に費やすことができるトークン数の上限のことです[2]
Gemini 2.5は全モデルで思考予算を利用できます。Flash-Liteではデフォルトでオフですが、Proではオフにできません。自動制御(auto)がデフォルト挙動になっている場合は、必要な思考予算をモデル側が自動で決定して推論します。

ここで注意するべきなのは思考の際に利用されるトークンは出力トークンと同じ単価で課金される[3]点です。前項の出力トークン数圧縮などの施策をして一見大丈夫そうでも、予想だにしない課金が発生することがあります。

この部分は出力トークン数をしっかり監視しないと見逃してしまいがちなので、特に注意が必要です。TypeScript(nodejs)を使う場合、以下のように予算を設定することができます。また、maxOutputTokensを設定する場合、これは思考予算を含めたものになるため、もし思考予算を利用する場合には設定する値にも注意します。例えば、maxOutputTokens <= thinkingBudgetとなる場合、出力が返ってくる前に応答が終了する場合があります。

思考予算の設定例.ts
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({
  project: 'sample-project-name',
  vertexai: true,
  location: 'global',
});

//以降、`ai`は上記で定義した`ai`を使いまわす

// サンプル用設定
const userPrompt = '...';

const response = await ai.models.generateContent({
  model: 'gemini-2.5-flash',
  contents: [{ role: 'user', parts: [{ text: userPrompt }] }],
  config: {
    maxOutputTokens: 256,
    thinkingConfig: { includeThoughts: false, thinkingBudget: 0 }, // 明示的にオフを宣言
  },
});

// 消費トークン数を監視する習慣をつけるとよい
const promptTokenCount = response.usageMetadata?.promptTokenCount ?? 0;
const thinkingTokenCount = response.usageMetadata?.thoughtsTokenCount ?? 0;
const outputTokenCount = response.usageMetadata?.candidatesTokenCount ?? 0;
console.debug(
  `消費トークン数: ↑ ${promptTokenCount}  (thinking: ${thinkingTokenCount}) / ↓ ${outputTokenCount}`
);

コンテキストキャッシュやバッチ予測に対応できないか?

指示する内容が多かったり、マルチモーダルな(画像や動画を利用するような)タスクを依頼する場合など、どうしても入出力トークン数を減らすことができない場合、コンテキストキャッシュ(context caching)やバッチ予測(batch prediction)を導入することで効果的に課金額を減らすことができる場合があります。

コンテキストキャッシュ

コンテキストキャッシュは、何度も使いまわすコンテキストをクラウド上でキャッシュすることができる機能です[4]。同じ指示の下で処理を繰り返す場合には、入力トークンに関する課金額と処理時間を減らすことができます。例えば、同じ動画に対する処理であったり、複雑なタスクを行う場合などに各プロンプトで共通する部分が2,048トークン以上ある場合には、これを使うことができます。このコンテキストキャッシングはLiteモデルも含めて様々なモデルで利用可能です。
この課金額へのインパクトは非常に大きく、通常の入力と比較して9割引となるため非常に省コスト化に貢献します。

実装する際の注意点として、コンテキストキャッシュを使える同士のタスクは可能な限り時間的に集中させるようにします。キャッシュにはTTL(有効期限、Time To Live)が設定されており、キャッシュが期限切れになる前に処理を完了させることで、再キャッシュのコストを回避できます。
処理に長時間必要な場合は、都度ai.caches.list()ai.caches.get()を使ってキャッシュが存在するか確認したり、ai.caches.update()で有効期限を延長するなどの処理が必要です。

コンテキストキャッシュを使った設定例.ts
import fs from 'fs';

const pdfBase64 = fs.readFileSync('temp/sample-file.pdf').toString('base64');

const cache = await ai.caches.create({
  model: 'gemini-2.5-flash',
  config: {
    systemInstruction: '文書の内容を要約してください。',
    contents: [
      {
        role: 'user',
        parts: [{ inlineData: { mimeType: 'application/pdf', data: pdfBase64 } }],
      },
    ],
    displayName: 'pdf-cache',
    ttl: '3600s',
  },
});

for (const lang of ['日本語', 'English']) {
  const response = await ai.models.generateContent({
    model: 'gemini-2.5-flash',
    contents: [{ role: 'user', parts: [{ text: `${lang}で要約してください` }] }],
    config: {
      cachedContent: cache.name,
      thinkingConfig: { includeThoughts: false, thinkingBudget: 0 },
    },
  });

  const usage = response.usageMetadata;
  console.debug(
    `[${lang}] 消費トークン数: ↑ ${usage?.promptTokenCount} (caching: ${usage?.cachedContentTokenCount ?? 0}) / ↓ ${usage?.candidatesTokenCount} (thinking: ${usage?.thoughtsTokenCount ?? 0})`
  );
  console.log(response.text);
}

結果を見てみると、大きくトークン数を節約できていることをメタデータからも確認できます。このように、特にファイルを読み込ませるようなタスクではコンテキストキャッシュは非常に有用です。

バッチ予測

バッチ予測は、大規模なデータ処理について非同期ではありますが高いスループットで行える機能です[5]。先ほどのコンテキストキャッシングは似たような作業を繰り返し行うときに有用でしたが、こちらのバッチ予測は単純に大量の処理を行うときに有用です[6]
バッチ予測を利用する場合、コンテキストキャッシュまでではないものの通常の課金額と比べて5割引となるため、こちらも無視できません。

これはCloud StorageやBigQuery上にあるデータについてのリクエストを送るとGeminiによる生成を非同期的に行い、結果を各クラウド上に自動で出力するというものです。ジョブが完了したかを別途確認する必要があるため実装コストは高くなる傾向がありますが、処理する内容を全て投げて十分な時間が経過してから別途取得する、といったように工夫すれば意外と手軽に利用することができるかもしれません。

公式によると24時間以内での対応を目指しているとのことですが、ボリュームにもよるものの多くの場合10分程度で完了します。また、1つのバッチには最大20万リクエストのデータを含めることができるため、非常に大きな処理もスマートに行うことができます[7]

試しにKaggleよりPublic Domainに設定されているSentiment Analysis Datasetを利用し、リクエストを送ってみます。

https://www.kaggle.com/datasets/abhi8923shriv/sentiment-analysis-dataset/data

データを前処理するためのコードたち(参考)

元のデータセットのままだと使えないので、前処理としてjsonlへの変換をPythonで、必要なプロパティの付与をTypeScriptでそれぞれ行いました。この先の記事では、これらのコードで前処理したjsonlファイルを用いるため、あらかじめCloud Storageの方にアップロードしておきます。

jsonlへの変換.py
import pandas as pd

df = pd.read_csv("test.csv")
df = df.rename(
    columns={
        "Time of Tweet": "timeOfTweet",
        "Age of User": "ageOfUser",
        "Country": "country",
    }
)
df = df[["textID", "text", "sentiment", "timeOfTweet", "ageOfUser", "country"]][:100]
df.to_json("input_unprocessed.jsonl", orient="records", lines=True)
リクエストフォーマットに整形.ts
import { Schema, Type } from '@google/genai';
import { readFileSync, writeFileSync } from 'node:fs';

const lines = readFileSync('temp/input_unprocessed.jsonl', 'utf-8').trim().split('\n');

const responseSchema: Schema = {
  type: Type.OBJECT,
  properties: {
    sentiment: {
      type: Type.STRING,
      enum: ['positive', 'neutral', 'negative'],
    },
    confidence: { type: Type.NUMBER, minimum: 0, maximum: 1 },
  },
};

const batchRequests = lines.map((line) => {
  const data = JSON.parse(line);
  const request = {
    model: 'gemini-2.5-flash',
    contents: [
      {
        role: 'user',
        parts: [
          {
            text: [
              '以下のテキストの感情を、付属する情報を加味して分析してください:',
              '```json',
              JSON.stringify(data),
              '```',
            ].join('\n'),
          },
        ],
      },
    ],
    generationConfig: {
      temperature: 0.0,
      responseSchema,
      responseMimeType: 'application/json',
      thinkingConfig: {
        includeThoughts: false,
        thinkingBudget: 0,
      },
    },
  };

  return {
    request,
  };
});

const output = batchRequests.map((r) => JSON.stringify(r)).join('\n');
writeFileSync('temp/input.jsonl', output);

単一のレコードが以下のようなフォーマットになっています。TypeScriptで実装するものとは若干プロパティ名が異なるため注意が必要です。

https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#request

{
   "request":{
      "model":"gemini-2.5-flash",
      "contents":[
         {
            "role":"user",
            "parts":[
               {
                  "text":"以下のテキストの感情を、付属する情報を加味して分析してください:\n```json\n{\"textID\":\"f87dea47db\",\"text\":\"Last session of the day  http://twitpic.com/67ezh\",\"sentiment\":\"neutral\",\"timeOfTweet\":\"morning\",\"ageOfUser\":\"0-20\",\"country\":\"Afghanistan\"}\n```"
               }
            ]
         }
      ],
      "generationConfig":{
         "temperature":0,
         "responseSchema":{
            "type":"OBJECT",
            "properties":{
               "sentiment":{
                  "type":"STRING",
                  "enum":[
                     "positive",
                     "neutral",
                     "negative"
                  ]
               },
               "confidence":{
                  "type":"NUMBER",
                  "minimum":0,
                  "maximum":1
               }
            }
         },
         "responseMimeType":"application/json",
         "thinkingConfig":{
            "includeThoughts":false,
            "thinkingBudget":0
         }
      }
   }
}
バッチ予測を使った設定例.ts
import { BatchJob, GoogleGenAI, JobState } from '@google/genai';

const startTime = Date.now();

// バッチ予測のタスクを作成
const batch = await ai.batches.create({
  model: 'gemini-2.5-flash',
  src: `gs://sample-bucket-name/input.jsonl`, // 入力はファイル名
  config: {
    dest: `gs://sample-bucket-name/output/`, // 出力はディレクトリ名
  },
});

console.log(`batch created: ${batch.name}`);
console.log(`batch state: ${batch.state}`);

// いずれかの状態になったら終了と判断
const completeStatuses: string[] = [
  JobState.JOB_STATE_SUCCEEDED,
  JobState.JOB_STATE_FAILED,
  JobState.JOB_STATE_CANCELLED,
  JobState.JOB_STATE_PAUSED,
];

// バッチ処理の完了を待機
let currentBatch: BatchJob = batch;
while (completeStatuses.includes(currentBatch.state ?? '') === false) {
  await new Promise((r) => setTimeout(r, 10 * 1000));
  currentBatch = await ai.batches.get({ name: currentBatch.name! });
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
  console.log(`[${elapsed}s] State: ${currentBatch.state}`);
}

const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
console.log('\nCompleted in ' + totalTime + 's');
console.log('Final state: ' + currentBatch.state);

動かしてみるとステータスがエラーがなければ概ね以下のように変化していきます。試しに100件の予測リクエストを含むバッチを実行してみましたが、成功ステータスの確認まで約4分かかりました。

  1. JOB_STATE_PENDING
  2. JOB_STATE_QUEUED
  3. JOB_STATE_RUNNING
  4. JOB_STATE_SUCCEEDED

先ほどのコードでは出力先をgs://sample-bucket-name/outputと指定していますが、実際のファイルはgs://sample-bucket-name/output/prediction-model-[timestamp]/predictions.jsonlに、特にformatで指定しない限りjsonl形式で出力されます。(一部プロパティは削除してあります。)

ラッパークラスで直接リクエストを送るよりも若干値の取り出しは不便ですが、response.candidates[0].content.parts[0].textに、確信度0.9でネガティブな感情であるとの判定結果がJSON文字列として格納されています

{
   "response":{
      "candidates":[
         {
            "content":{
               "parts":[
                  {
                     "text":"{\"confidence\":0.9,\"sentiment\":\"negative\"}"
                  }
               ],
               "role":"model"
            },
            "finishReason":"STOP"
         }
      ],
      "usageMetadata":{
         "candidatesTokenCount":11,
         "promptTokenCount":88,
         "totalTokenCount":99,
         "trafficType":"ON_DEMAND"
      }
   },
}

まとめ

本記事では、Gemini API on Vertex AIを低コストで運用するための3つの主要な観点について、実装例とともに紹介しました。

  1. 入出力トークン数の最適化: 最も基本的ながら効果の高い方法です。入力プロンプトの簡潔化や、出力形式の工夫(例:JSON全文ではなくindexを返す)により、トークン数そのものを削減します。
  2. 思考予算(thinking budget)の管理: 出力トークンと同じ単価で課金されるため、見落としがちなコスト要因です。複雑なタスクでない限り、明示的に予算を0に設定(thinkingBudget: 0)することで、意図しない課金を防ぎます。
  3. 高度な機能の活用(コンテキストキャッシュとバッチ予測): 大量の処理や共通のコンテキストを扱う場合に劇的なコスト削減(最大9割引)を実現します。コンテキストキャッシュは繰り返しのタスクに、バッチ予測は非同期での大量処理に適しています。

Gemini APIは非常に強力ですが、モデルや機能ごとの特性を理解し、ユースケースに合わせてこれらの設定を適切にチューニングすることが、コスト効率の高い運用に不可欠です。本記事が、Vertex AIを活用した開発・運用の一助となれば幸いです。

さいごに

私たちキーウォーカーは 「まずは試す」「成功も失敗もオープンに共有する」 ことを大切にしています。

新しい技術やツールを積極的に取り入れ、事業成果/顧客価値に結びつける取り組みを続けています。

  • 新しいことに主体的に取り組みたい
  • 最新技術(生成AIやLLMなど)を活用した価値創出へチャレンジしたい
  • BizとDevの垣根を越えて挑戦してみたい

新卒・中途問わず、そんな意欲ある仲間を歓迎しています。
少しでも興味を持っていただいた方は、ご応募・お問い合わせください。

👉 詳しくは 新卒採用ページ中途採用ページをご覧ください。

脚注
  1. https://cloud.google.com/vertex-ai/generative-ai/pricing ↩︎ ↩︎

  2. https://docs.cloud.google.com/vertex-ai/generative-ai/docs/thinking ↩︎

  3. 価格表では'reasoning'と書かれています。 ↩︎

  4. https://docs.cloud.google.com/vertex-ai/generative-ai/docs/context-cache/context-cache-overview ↩︎

  5. https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/batch-prediction-gemini ↩︎

  6. Gemini 2.5シリーズではバッチ予測を利用する際にも暗黙的なコンテキストキャッシュが利用でき、優先して適用されます。 ↩︎

  7. https://docs.cloud.google.com/vertex-ai/docs/quotas ↩︎

株式会社キーウォーカー テックブログ

Discussion