1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【実測】OCI Enterprise AI は Prompt Cache を使える?Responses API の usage で2モデルを検証してみた

1
Posted at

はじめに

LLM に毎回同じ長いシステム指示や参考資料を渡すと、「この部分はさっきも送ったのに、毎回同じ費用がかかるのかな?」と気になります。

そこで今回は、OCI Enterprise AI の Responses API に実際にリクエストを送り、Prompt Caching が使われたことを API の応答で確認してみました。速度だけで判断せず、キャッシュされた入力 token 数を読み取ります。

結論から言うと、今回の環境では xai.grok-4.3 と openai.gpt-oss-120b の両方でキャッシュ命中を観測しました。ただし、命中と料金の割引は分けて確認する必要がありました。

この記事では、次の内容を紹介します。

  • Prompt Caching の仕組みと、回答そのもののキャッシュとの違い
  • .env と model-settings.json を使う Python の検証コード
  • 2モデル・24リクエストの実行結果と、成功しなかったパラメーター
  • OCI の普通入力/キャッシュ入力の価格差と、実測値に基づく試算

検証日は 2026年11月5日です。以下の表は実測値であり、モデル全体の性能保証ではありません。

1. Prompt Caching とは?

長い資料を読む作業を考えると分かりやすいです。1回目に資料を読み、2回目に同じ資料で別の質問をするとき、資料の読み直しに相当する計算の一部を再利用できる仕組みが Prompt Caching です。技術的には、共通する入力の先頭部分を処理した中間状態を再利用します。

前の回答をそのまま返す「回答キャッシュ」とは異なり、回答の生成は毎回行います。 そのため、入力の一部がキャッシュされても、出力 token の費用がなくなるわけではありません。

静的な指示や資料を先頭へ、変化する質問を後ろへ置くと、共通部分を保ちやすくなります。一方、キャッシュの保持やルーティングによって、同じ入力でも命中が保証されるとは限りません。これは一般的な仕組みの説明です。xAI 直接 API の推奨ヘッダーや保持条件が、そのまま OCI にも適用されるとは扱いません。xAI の Prompt Caching 解説

OCI の Grok 4.3 の公式説明にも cached input token の対応が記載されています。今回は、その記載に加えて、OCI の Responses API から実際の使用量を取得して確認しました。Oracle: xAI Grok 4.3

2. 今回の検証条件

項目 設定
Python 3.12
呼び出し OCI 公式の推論エンドポイントの POST /openai/v1/responses
認証 .env の接続別 API key と JSON の Project OCID
モデル xai.grok-4.3 / openai.gpt-oss-120b
入力 公開可能な合成データ170行。実測 3,488 / 3,539 tokens
出力上限 リクエストには max_output_tokens=128 を指定
推論設定 両モデルとも reasoning.effort=low
保存 store=false、過去の response ID は不使用
繰り返し 各モデル12回。自動リトライなし、リクエスト間に約1秒待機
計時 HTTP 応答全体の所要時間。TTFT は未計測

設定ファイルには別サービスの互換エンドポイントを使う3モデルもありましたが、OCI の検証対象から除外し、呼び出していません。また、初回にも少量の cached token があったため、「完全なコールド状態を作った」とは言えません。

store=false は応答保存の設定です。今回、それを指定していてもキャッシュ token を観測できました。保存された会話の再利用と Prompt Caching は分けて考えます。

3. .env とモデル JSON を準備する

プロジェクトでは、秘密情報を .env、モデル名や接続先などを model-settings.json に分けています。検証コードは既存ファイルを読み取るだけで、設定を書き換えません。

PLATFORM_MODEL_SETTINGS_FILE=model-settings.json
PLATFORM_OCI_ENTERPRISE_AI_API_KEY=<primary-api-key>
PLATFORM_OCI_ENTERPRISE_AI_SECONDARY_API_KEY=<secondary-api-key>

JSON の最小構成は次のとおりです。<region> と Project OCID は自分の値に置き換えてください。サンプルには実際の接続先や秘密情報を入れていません。

{
  "version": 6,
  "enterprise_ai": {
    "api_path": "/responses",
    "connections": [
      {
        "connection_id": "primary",
        "endpoint": "https://inference.generativeai.<region>.oci.oraclecloud.com/openai/v1",
        "project_ocid": "<your-project-ocid>"
      }
    ],
    "models": [
      {
        "model_id": "xai.grok-4.3",
        "connection_id": "primary",
        "reasoning_effort": "low"
      }
    ]
  }
}

もう一方のモデルも測る場合は、同梱の model-settings.example.json を使います。JSON の接続設定を使い、API key だけを接続 ID に対応する環境変数から取得します。プロセスの環境変数は .env より優先します。PLATFORM_MODEL_SETTINGS_FILE の相対パスは .env のディレクトリから解決します。

OCI の公式資料では、互換 API の base URL、API key、Project OCID を指定して Responses API を呼び出す構成が説明されています。Oracle: OCI Responses API

4. Python で実際に呼び出す

全ソースは ZIP 内の probe.py にあります。重要な部分は、同じリクエストを送って、返された usage をそのまま保存するところです。

import httpx

with httpx.Client(timeout=180, follow_redirects=False) as client:
    response = client.post(
        endpoint.rstrip("/") + "/responses",
        headers={
            "Authorization": f"Bearer {api_key}",
            "OpenAI-Project": project_ocid,
            "Content-Type": "application/json",
        },
        json={
            "model": model_id,
            "input": [
                {"role": "system", "content": fixed_prefix},
                {"role": "user", "content": question},
            ],
            "reasoning": {"effort": "low"},
            "max_output_tokens": 128,
            "store": False,
        },
    )
    response.raise_for_status()
    usage = response.json().get("usage", {})
    details = usage.get("input_tokens_details", {})
    cached_tokens = details.get("cached_tokens")
    print({"usage": usage, "cached_tokens": cached_tokens})

上は要点の抜粋です。完全版では認証情報をログに出さず、HTTP 400 の本文も脱敏して保存します。キャッシュ項目がない場合を 0 に置き換えず、「情報不足」と判定するようにしました。

実行手順は次のとおりです。--output は既存の記録を上書きしないよう、新しいディレクトリを指定します。

uv venv --python 3.12
uv pip install --python .venv/bin/python -r requirements.txt
cp .env.example .env
cp model-settings.example.json model-settings.json
# .env と model-settings.json に自分の設定を入れる
.venv/bin/python -m unittest -v test_probe.py
.venv/bin/python probe.py --env-file .env --output results-new
.venv/bin/python make_report.py --results results --output report.html

既存プロジェクトの設定を使う場合は、次のように指定できます。

.venv/bin/python probe.py   --env-file /path/to/project/platform/.env   --output results-new

実測では、既存の Python 環境を利用して次のコマンドを実行しました。

rag/backend/.venv/bin/python tmp/oci-prompt-cache/probe.py   --env-file platform/.env   --output tmp/oci-prompt-cache/live-results

5. 実行結果:両モデルで命中を確認

ケースは以下の順に実行しました。

  1. A の初回入力と、同一入力の繰り返し2回
  2. 固定資料を保ち、末尾の質問だけ変更2回
  3. 資料先頭付近の識別子を B に変更し、同じ B を再送
  4. A に戻す
  5. 同じ prompt_cache_key を付けて2回
  6. prompt_cache_retention="in-memory" を加えて2回

同一入力の繰り返しで返された usage の実例です。

{
  "model": "xai.grok-4.3",
  "case": "A_repeat_1",
  "usage": {
    "input_tokens": 3488,
    "output_tokens": 151,
    "total_tokens": 3639,
    "input_tokens_details": {"cached_tokens": 3456},
    "output_tokens_details": {"reasoning_tokens": 149}
  }
}

ここでの model と case は、応答の usage に検証用のラベルを付けたものです。元の応答全体は ZIP の results/model-01/02-A_repeat_1.response.json に保存しています。

input_tokens=3488 のうち 3456 がキャッシュ由来なので、この要求の入力 token の約99.08%でキャッシュが観測されています。

以下は全24回の結果です。秒数は小数第3位まで記録しました。

モデル ケース HTTP 入力 cached 出力 全体秒数
xai.grok-4.3 A_initial 200 3488 128 131 2.071
xai.grok-4.3 A_repeat_1 200 3488 3456 151 1.897
xai.grok-4.3 A_repeat_2 200 3488 3456 156 1.868
xai.grok-4.3 A_suffix_1 200 3488 128 125 1.812
xai.grok-4.3 A_suffix_2 200 3488 128 205 2.062
xai.grok-4.3 B_prefix_change 200 3488 128 137 1.674
xai.grok-4.3 B_repeat 200 3488 3456 137 1.495
xai.grok-4.3 A_return 200 3488 3456 150 1.941
xai.grok-4.3 cache_key_1 200 3488 3456 159 1.853
xai.grok-4.3 cache_key_2 200 3488 3456 198 2.180
xai.grok-4.3 retention_1 400 — — — 0.185
xai.grok-4.3 retention_2 400 — — — 0.164
openai.gpt-oss-120b A_initial 200 3539 64 18 0.178
openai.gpt-oss-120b A_repeat_1 200 3539 3536 20 0.114
openai.gpt-oss-120b A_repeat_2 200 3539 3536 18 0.112
openai.gpt-oss-120b A_suffix_1 200 3539 3520 18 0.110
openai.gpt-oss-120b A_suffix_2 200 3539 3520 18 0.110
openai.gpt-oss-120b B_prefix_change 200 3539 96 18 0.165
openai.gpt-oss-120b B_repeat 200 3539 3536 18 0.109
openai.gpt-oss-120b A_return 200 3539 3536 18 0.108
openai.gpt-oss-120b cache_key_1 200 3539 3536 18 0.106
openai.gpt-oss-120b cache_key_2 200 3539 3536 18 0.106
openai.gpt-oss-120b retention_1 400 — — — 0.015
openai.gpt-oss-120b retention_2 400 — — — 0.010

同じ質問をそのまま再送したケースでは、両モデルとも大きなキャッシュ命中を観測しました。

一方、末尾の質問だけ変更したケースでは差がありました。gpt-oss-120b は3,520 tokensがキャッシュ由来でしたが、Grok 4.3 は128 tokensでした。今回の結果から、Grok 4.3 で長い資料の前方一致が必ず再利用されるとは言えません。ルーティングや内部の処理が原因かどうかも、この応答だけでは特定できません。

速度は補助情報です。たとえば Grok 4.3 は初回2.071秒、同一入力の再送1.897秒でしたが、出力 token 数も変わっています。少数試行であり、統計的な高速化率や TTFT の改善は主張しません。

report-desktop.png

上の画像は、保存済み API 応答から作った HTML を実際にブラウザで開いて撮影したものです。OCI コンソールの画面ではありません。

6. キャッシュ用パラメーターの落とし穴

prompt_cache_key を付けた要求は、両モデルで HTTP 200 になりました。ただし、キーなしでも既に命中しているため、この結果だけでキーのルーティング効果や必須性を証明したとは扱いません。

prompt_cache_retention="in-memory" は、両モデルとも HTTP 400 でした。返されたエラーは次のとおりです。

unknown variant `in-memory`, expected `in_memory` or `24h`

このエラーは「キャッシュ非対応」を意味しません。指定値のハイフンとアンダースコアの違いが問題でした。 今回の呼び出し上限に達したため、in_memory と 24h の受理・保持時間・課金は追加実測していません。

同梱コードの既定値は実測の再現用として in-memory を保っています。後続検証では --retention-value in_memory または --retention-value 24h を指定できますが、その結果は本記事の実測表には含めません。

なお、max_output_tokens=128 を要求しましたが、Grok の usage.output_tokens には128超の応答もありました。指定値が実際の使用量を必ず128以下に抑えた、とは言えません。料金計算は応答の実測 usage を使います。

7. OCI の料金:キャッシュなしとありで何が変わる?

2026年11月5日に Oracle Cloud Price List と Oracle の公開価格 JSON を確認しました。以下は USD、Pay As You Go、100万 tokens あたりの公開価格です。Grok は今回の約3,500入力 tokensに対応する標準・短いコンテキストの価格を使います。Priority の単価ではありません。

モデル 普通入力 キャッシュ入力 出力
Grok 4.3 US$1.25 US$0.20 US$2.50
gpt-oss-120b US$0.15 独立した単価を確認できず US$0.60

確認した SKU は Grok が B112080 / B112081 / B112082、gpt-oss が B112004 / B112005 です。抽出した公式データは pricing-source.json、計算用の値は pricing.json に残しています。

Grok 4.3 のキャッシュ部分の入力単価は、普通入力に対して 84%低い計算です。

1 - 0.20 / 1.25 = 0.84

これは「リクエスト全体の費用が84%安くなる」という意味ではありません。キャッシュされない入力と出力はそれぞれの単価で計算します。

N = 入力 token 数(キャッシュ分を含む)
C = cached token 数
O = 出力 token 数

キャッシュなしの仮定:
  N × 普通入力単価 / 1,000,000 + O × 出力単価 / 1,000,000

実測のキャッシュ量を適用:
  (N-C) × 普通入力単価 / 1,000,000
  + C × キャッシュ入力単価 / 1,000,000
  + O × 出力単価 / 1,000,000

実測の A_repeat_1 を例にすると、Grok 4.3 は N=3488、C=3456、O=151 でした。

計算対象 キャッシュなしの仮定 実測キャッシュ量を適用
入力費用 US$0.0043600 US$0.0007312
出力費用 US$0.0003775 US$0.0003775
合計 US$0.0047375 US$0.0011087

この1回では、合計の試算差は約76.60%です。出力の reasoning token は API の output_tokens に含まれるので、再度足して二重計上しません。キャッシュなし側は同じ入出力 token 数を通常単価で計算した反実仮想であり、キャッシュを強制無効化して実測した請求ではありません。

成功した Grok の10回分を合算すると、入力34,880、キャッシュ21,248、出力1,549 tokensでした。公開単価での合計は、キャッシュなしの仮定で US$0.0474725、実測キャッシュ量を適用すると US$0.0251621 です。計算は cost_example.py で再現できます。

gpt-oss-120b は、キャッシュ命中を観測しましたが、確認した公式一覧に独立したキャッシュ入力単価がありません。したがって、キャッシュ分を無料や割引とは仮定しません。入力の全量を普通入力単価で計算する参考額は出せますが、キャッシュによる料金差は未確認です。

これらは公開単価による試算で、請求書との照合結果ではありません。税・契約割引・通貨換算・追加のサービス料金は含めず、HTTP 400 の課金の有無も確認していません。価格表にはコンテキスト境界の表記差もあるため、本記事では十分小さい入力のケースに限定しています。利用前には最新の料金表と契約条件を確認してください。

8. 分かったこと

今回、OCI の Responses API 経由で、Grok 4.3 と gpt-oss-120b の両方に cached_tokens > 0 があることを実測できました。キャッシュ対応の確認には、体感速度より usage の記録が役立ちます。

実装では固定部分を安定させ、質問を後ろに置き、毎回 input_tokens・cached_tokens・output_tokens を記録する構成にすると、効果を後から確認できます。

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?