課金と透明性

AIゲートウェイの透明性監査:プロンプト注入・架空キャッシュと真のAPIコスト

格安プロキシに潜む隠れプロンプト注入と架空キャッシュの手口を検証。マルチターン会話のトークン水増しを比較し、オープンソーススクリプトで実態を照合します。

「格安」の裏に潜む真の請求額:隠れプロンプト注入と架空キャッシュの罠

AIゲートウェイやLLMリレープロキシ市場では、公式価格の50〜80%引きを謳う大幅な「割引」が頻繁に宣伝されています。しかし詳細な業界ベンチマーク調査によって、一見安価に見える中継プロバイダーの多くが、ユーザーに無断で膨大なシステムプロンプトを注入し、架空のキャッシュ数値を捏造することで、請求トークン数を大幅に水増ししている実態が明らかになりました。

上流プロキシが無断で隠し指示文を注入したりトークン集計を改ざんすると、本来1,500トークン程度のリクエストが15,000〜28,000トークンとして過大請求されます。マルチターン会話では、この水増しがターンを重ねるごとに雪だるま式に悪化し、8ターン目には実際の文脈サイズの4倍以上に膨れ上がります。「低倍率」に見えた供給元が、実際には200%から400%も高額になり、会話が長くなるほどコストが激増します。

AIプロキシ比較:アーキテクチャとトークン計量の信頼性
指標/項目一般的な格安プロキシAPI Route ゲートウェイ
プロンプト注入未告知の隠れシステムトークンを5,000〜20,000件注入0トークン注入(リクエストのバイト列を完全透過)
初回セッションキャッシュ新規セッションの初回から20,000件以上の架空キャッシュ読取を報告0架空キャッシュ読取(接頭辞ツリーの完全一致に厳密準拠)
キャッシュ書込の再現性硬直した計算式(約18%の固定書き込み比率)純粋なプロンプト差分のみ(新規追加文脈のみ書き込み)
マルチターン文脈増分1ターンごとに10Kトークン超激増(第8ターンで4.2倍に膨張)真の文脈成長と完全一致(1ターンあたり約1.46Kトークン増)
トークナイザー整合性 (GPT/Claude)組織的な1.30倍加算や水増しトークン集計1.00倍の完全一致(公式トークナイザーと100%合致)
検証用エンドポイント完全遮断またはHTTP 404応答/v1/messages/count_tokens をネイティブ提供
実質的な利用コスト見かけは格安でも総請求額は2〜4倍高額透明・検証可能・数学的に正確な課金

API Routeは改ざんゼロ・注入ゼロの完全透過基準を徹底しています。料金一覧で透明な単価をご確認いただき、アカウント登録のうえクイックスタートから安全な少額検証をお試しください。

プロキシ課金トラップの技術的構造

下流のリレープロバイダーを網羅的に監査した結果、開発者から不正にトークン費用を徴収する代表的な手口が3点確認されました。

罠1:第0ターンの「不可能なキャッシュ読み取り」

Anthropicプロンプトキャッシュ技術仕様およびOpenAIプロンプトキャッシュ解説で規定されているとおり、プレフィックス型プロンプトキャッシュは完全なバイト単位の先頭一致を前提とします。プロンプト先頭に暗号学的にユニークなUUID乱数を配置した場合、サーバー上の既存キャッシュにヒットすることは物理的に不可能です。しかし競合プロキシは、全く新しいセッションの第0ターンから21,800〜22,372トークンの cache_read_input_tokens を計上します。これは請求のベースを吊り上げるための完全な捏造です。

罠2:固定比率による架空キャッシュ計算式

正当なプロンプトキャッシュでは、新たに追加されたテキストのみが cache_creation_input_tokens に記録され、過去の共通文脈が cache_read_input_tokens に記録されます。競合プロキシでは、ユーザーが加えたテキストが50トークンであろうと1,500トークンであろうと、書き込み割合が総トークンの約18%(Cache Write / Total Tokens ≡ 18.00%)に固定されています。実態のキャッシュ稼働とは無関係に、固定コードによってトークン数を自動生成している動かぬ証拠です。

罠3:マルチターン対話での複利的なトークン爆発

エージェントや対話フローでは、ターンごとに一定量の新しい文脈が追加されます。競合プロキシでは、隠し注入と架空キャッシュによりトークン数が急激に膨れ上がります。

Expected Growth:   [Context: 13.2K] ──(+1.46K)──► [14.6K] ──(+1.46K)──► [16.1K] ──(+1.46K)──► [17.6K] ... Turn 8: [23.4K]
Inflated Growth:   [Reported: 28.7K] ──(+10.7K)──► [33.4K] ──(+10.7K)──► [44.1K] ──(+10.7K)──► [54.7K] ... Turn 8: [97.4K] (4.2x)

毎ターン10,600トークン以上が機械的に加算される結果、真のコンテキストが23,404トークンしかない8ターンの対話に対して、競合プロキシでは97,400トークンもの請求が発生します。名目上の割引は完全に吹き飛び、長期的なエージェント作業では極めて割高な請求となります。

オープンソースのPython監査テストスクリプト

真の透明性は、独立した技術者による客観的な検証に耐えうるものでなければなりません。以下のPython 3スクリプトは外部依存関係ゼロで動作し、マルチターンの検証手順を完全に自動再現します。https://global.api-route.com や任意の第三者プロキシに対して直接実行し、トークンの真実性を測定できます。

#!/usr/bin/env python3
"""
AI Gateway Token Usage & Cache Fidelity Audit Harness
Open Source Tool by API Route (https://www.api-route.com)

Usage:
  BASE_URL=https://global.api-route.com API_KEY=sk-... python3 usage_test.py claude-sonnet-5-5 gpt-6-sol

Optional Environment Variables:
  DIALOGS=2    Number of independent conversation sessions (default: 2)
  TURNS=8      Number of consecutive turns per session (default: 8)
  GAP=30       Seconds between turns within ephemeral cache window (default: 30)
"""

import json
import os
import sys
import time
import urllib.error
import urllib.request
import uuid

BASE = os.environ.get('BASE_URL', 'https://global.api-route.com').rstrip('/')
KEY = os.environ.get('API_KEY', '')
DIALOGS = int(os.environ.get('DIALOGS', 2))
TURNS = int(os.environ.get('TURNS', 8))
GAP = int(os.environ.get('GAP', 30))


def post(path, body):
    """Executes an authenticated JSON POST request."""
    req = urllib.request.Request(
        BASE + path,
        data=json.dumps(body).encode('utf-8'),
        headers={
            'Content-Type': 'application/json',
            'User-Agent': 'api-route-transparency-audit/1.0',
            'x-api-key': KEY,
            'Authorization': f'Bearer {KEY}',
            'anthropic-version': '2023-06-01',
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=180) as r:
            return r.status, json.load(r)
    except urllib.error.HTTPError as e:
        return e.code, {'error': e.read().decode('utf-8', errors='replace')[:200]}


def claude_turn(model, system, msgs):
    """Executes a Claude turn, inspecting both /v1/messages and /v1/messages/count_tokens."""
    body = {
        'model': model,
        'max_tokens': 30,
        'system': [
            {'type': 'text', 'text': system, 'cache_control': {'type': 'ephemeral'}}
        ],
        'messages': msgs,
    }
    count_status, count = post(
        '/v1/messages/count_tokens',
        {k: body[k] for k in ('model', 'system', 'messages')},
    )
    status, d = post('/v1/messages', body)
    u = d.get('usage') or {}
    text = ''.join(b.get('text', '') for b in d.get('content', []) if isinstance(b, dict)) or '-'
    counted = count.get('input_tokens') if count_status == 200 else f'HTTP {count_status}'
    inp = u.get('input_tokens', 0)
    read = u.get('cache_read_input_tokens', 0)
    write = u.get('cache_creation_input_tokens', 0)
    out = u.get('output_tokens', 0)
    return status, counted, inp, read, write, out, text, d.get('error')


def gpt_turn(model, system, msgs, cache_key):
    """Executes a GPT turn on /v1/responses with prompt_cache_key."""
    body = {
        'model': model,
        'instructions': system,
        'input': msgs,
        'max_output_tokens': 40,
        'prompt_cache_key': cache_key,
    }
    status, d = post('/v1/responses', body)
    u = d.get('usage') or {}
    det = u.get('input_tokens_details') or {}
    read = det.get('cached_tokens', 0)
    write = det.get('cache_write_tokens', 0)
    raw_input = u.get('input_tokens', 0)
    inp = max(0, raw_input - read - write)
    out = u.get('output_tokens', 0)
    text = ''
    for item in d.get('output', []):
        if isinstance(item, dict):
            for c in item.get('content') or []:
                if isinstance(c, dict):
                    text += c.get('text', '')
    return status, None, inp, read, write, out, text or '-', d.get('error')


def run_audit(model):
    is_claude = model.startswith('claude')
    print(f"AUDITING MODEL: {model} | Sessions: {DIALOGS} | Turns/Session: {TURNS} | Turn Gap: {GAP}s")
    for dialog in range(DIALOGS):
        nonce = uuid.uuid4().hex
        system = f'Session {nonce}. ' + ' '.join(
            f'Rule {i}: service number {i} returns code {i * 7} and writes log to data{i}.'
            for i in range(1, 320)
        )
        msgs = []
        for turn in range(TURNS):
            if turn > 0:
                time.sleep(GAP)
            extra = ' '.join(
                f'Note {turn}.{j}: audit service {j + turn} and record status.'
                for j in range(60)
            )
            question = f'{extra} What is the code for service {turn + 3}? Output number only.'
            if is_claude:
                msgs.append({
                    'role': 'user',
                    'content': [{'type': 'text', 'text': question, 'cache_control': {'type': 'ephemeral'}}],
                })
                status, counted, inp, read, write, out, text, error = claude_turn(model, system, msgs)
                msgs[-1]['content'][0].pop('cache_control', None)
            else:
                msgs.append({'role': 'user', 'content': question})
                status, counted, inp, read, write, out, text, error = gpt_turn(
                    model, system, msgs, f'audit-{nonce}'
                )
            total = inp + read + write
            print(
                f"[{model}] S{dialog} T{turn} | HTTP {status} | "
                f"CountTokens: {str(counted):>10} | Input: {inp:5d} | "
                f"CacheRead: {read:5d} | CacheWrite: {write:5d} | Total: {total:6d} | Out: {out:3d}"
                + (f" | ERROR: {error}" if status != 200 else "")
            )
            if status == 200:
                msgs.append({'role': 'assistant', 'content': text})
            else:
                msgs.pop()


if __name__ == '__main__':
    if not KEY:
        print("ERROR: API_KEY environment variable is required.", file=sys.stderr)
        sys.exit(1)
    models_to_test = sys.argv[1:] or ['claude-sonnet-5-5', 'gpt-6-sol']
    for m in models_to_test:
        run_audit(m)

完全なオープンソース検証ツール、ドキュメント、実行例はGitHubの GitHub benchmark repository で公開されています。

マルチターン会話の実測ベンチマーク結果

同一のプロンプトシーケンスを用い、一般的な「格安」プロキシとAPI Routeの本番ゲートウェイに対して比較検証を実施しました。Claude OpusおよびSonnetにおける8ターン連続対話の実測値は以下のとおりです。

Claude マルチターン会話ベンチマーク:他社プロキシ vs API Route
ターン真のコンテキスト増分他社プロキシ報告合計他社キャッシュ読み取り他社キャッシュ書き込みAPI Route 報告合計API Route キャッシュ読み取りAPI Route キャッシュ書き込み
第0ターン13,177 トークン28,68222,372(架空)5,162 (18.0%)13,177(正確)0(忠実)13,173
第1ターン14,638 トークン33,39726,7186,011 (18.0%)14,638(正確)014,634
第2ターン16,099 トークン44,06035,2497,931 (18.0%)16,099(正確)14,6341,461
第3ターン17,560 トークン54,72643,2349,850 (18.0%)17,560(正確)16,0951,461
第4ターン19,021 トークン65,39352,31511,770 (18.0%)19,021(正確)17,5561,461
第5ターン20,482 トークン76,06360,85013,692 (18.0%)20,482(正確)19,0171,461
第6ターン21,943 トークン86,73065,91415,612 (18.0%)21,943(正確)20,4781,461
第7ターン23,404 トークン97,40074,02317,532 (18.0%)23,404(正確)21,9391,461

実測から判明した重要な事実:

  • 第0ターンの架空読取ゼロ:API Routeは初回セッションで厳密に0を返し、コールドスタートを忠実に反映します。競合は22,000以上の読取を捏造しました。
  • 増分の完全一致:各ターンで追加されるノートは正確に1,461トークンです。API Routeの書き込みは1,461トークンを正確に記録しますが、競合は毎ターン10,660トークン以上膨張します。
  • 実質コストの逆転:第7ターン時点で真の文脈は23,404トークンですが、競合は97,400トークンを請求します。競合が仮に50%引きだとしても48,700請求単位となり、API Routeの通常価格23,404単位と比べて2倍以上高額になります。

OpenAI GPTの1.00倍トークナイザー完全整合

gpt-6-sol などの検証でも、競合プロキシはユーザー入力に対して機械的に1.30倍の係数を掛けて水増ししていました(第0ターンで基準8,258に対し10,724を請求)。API Routeは入力プロンプトへの水増しを一切行わず、公式トークナイザーと100%合致しています。

開発者向けチェックリスト:不正プロキシを見抜く4つの手順

ご利用中のAIプロキシで過剰請求が行われていないか、以下の4手順で簡単に検証できます。

  1. 初回コールドキャッシュの検証:システムプロンプトの先頭に32桁のランダムUUIDを付与して送信します。初回で cache_read_input_tokens > 0 が返る場合、架空キャッシュです。
  2. キャッシュ書き込み比率の確認:複数ターンで書き込み量と総入力の比率を計算します。追加テキスト長に関わらず18.00%前後の固定値を示す場合、偽造計算式が使われています。
  3. 文脈増分とトークナイザーの照合:第Nターンで追加したテキストのトークン数を測定します。実追加が1,000トークンであるにもかかわらず、プロキシの報告が5,000〜10,000トークン増えている場合は、隠れプロンプトの注入が行われています。
  4. 公式事前カウントAPIの照会:ペイロードを /v1/messages/count_tokens エンドポイントへ送信して確認します。
curl -X POST https://global.api-route.com/v1/messages/count_tokens \
  -H "Content-Type: application/json" \
  -H "x-api-key: $API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "claude-sonnet-5-5",
    "messages": [{"role": "user", "content": "Hello, world!"}]
  }'
{
  "input_tokens": 10
}

このエンドポイントがHTTP 404を返すか、請求トークン数と乖離しているプロバイダーは監査不能です。API Routeでのご利用時は、利用ログですべてのリクエストの内訳をリアルタイムに照合できます。