Vercel AI SDK × Stripe V2 API で効率的にAI利用量を計測する実装方法
この記事では、Vercel AI SDKとStripe V2 APIを組み合わせた高スループットなAI利用量計測の実装方法を解説しています。Stripe V2 APIの新機能であるMeter EventStream APIを活用することで、秒間10,000リクエストまでの処理が可能になり、大規模なAIサービスでの利用量計測に適しています。
目次
最近のAIサービス開発において、APIの利用量計測は非常に重要な要素となっています。特に、AIモデルのトークン使用量を正確に計測し、ユーザーごとに課金するシステムを構築する場合、効率的なAPI呼び出しが求められます。
このような状況において、Stripeが提供する利用量ベースの課金APIは非常に便利なツールですが、最近提供されたV2 APIには多くの改善点があります。今回は、このStripe V2 APIを活用してVercel AI SDKと組み合わせた、高スループットなAI利用量計測の実装方法をご紹介します。
Stripeの利用量計測APIの変遷とその違い
2025/02/21時点で、StripeのAgent Toolkit SDKを利用した利用量計測は、v1のMeter Events APIを使っています。

v1のMeter Event APIも秒間1,000リクエストまで処理できますが、V2 APIにあるMeter EventStream APIを使用すると、その上限が10,000まで大幅に増加します。これは特に、多数のユーザーが同時にAIモデルを使用するようなサービスでは大きなメリットとなります。
実装を始める前の準備
V2 APIを使用する前に、いくつかの準備が必要です:
- Stripeアカウントを持っていること
 - プロジェクトで使用するメーター(meter)を作成していること
 - Vercel AI SDKを使ったアプリケーションの基本構造があること
 
また、以下の環境変数を設定しておく必要があります:
STRIPE_SECRET_API_KEY: StripeのシークレットAPIキーSTRIPE_CUSTOMER_ID: 利用量を計測するStripeカスタマーIDSTRIPE_METER_NAME_INPUT: 入力トークン用のメーター名STRIPE_METER_NAME_OUTPUT: 出力トークン用のメーター名CLAUDE_API_KEY: AnthropicのAPIキー(使用するAIモデルによって異なります)
Meter EventsとMeter EventStreamでの実装の違い
API Rate Limitの違いだけでなく、ステートレスな認証セッションが必要になることもポイントです。15分を有効期限とするセッションを作成し、有効期限が切れるまではそのセッションで発行された認証トークンを使ってStripe APIを呼び出します。そのため、このAPIを使うには、簡易的なステートを保持する場所が必要ですので、Cookieなどを利用することになるでしょう。
最小構成はこのようになります。Stripeのクラスを2回初期化することや、Meter EventStream APIを呼び出す側のStripeクラスは、v2.billing.meterEventSessionで取得したauthentication_tokenを使うことに注意です。
const meterEventSession = await new Stripe(config.secretKey, {
  apiVersion: '2025-01-27.acacia'
}).v2.billing.meterEventSession.create();
const stripe = new Stripe(meterEventSession.authentication_token)
実際には、ここにmeterEventSession.idやmeterEventSession.expires_atを利用したCookie等のセッションを作って、再利用またはセッションの作り直しを行うことになります。
独自の計測middlewareを作る
v2のAPIにはAI AgentToolkit SDKが対応していません。そのためSDKの実装をもとに自前で実装します。
以下は、Vercel AI SDK用にカスタマイズした使用量ベース課金のmiddlewareです:
type StripeUsageBasedBillingMiddlewareConfig = {
  secretKey: string
  billing?: {
    type?: 'token';
    customer: string;
    meters: {
      input?: string;
      output?: string;
    };
  };
};
export const createStripeUsageBasedBillingMiddleware = (config: StripeUsageBasedBillingMiddlewareConfig): LanguageModelV1Middleware => {
  const bill = async ({
    promptTokens,
    completionTokens,
  }: {
    promptTokens: number;
    completionTokens: number;
  }) => {
    const meterEventSession = await new Stripe(config.secretKey, {
      apiVersion: '2025-01-27.acacia'
    }).v2.billing.meterEventSession.create();
    const stripe = new Stripe(meterEventSession.authentication_token)
    
    if (config.billing) {
      if (config.billing.meters.input) {
        await stripe.v2.billing.meterEventStream.create({
          events: [{
            event_name: config.billing.meters.input,
            payload: {
              stripe_customer_id: config.billing.customer,
              value: promptTokens.toString(),
            }
          }]
        });
      }
      if (config.billing.meters.output) {
        await stripe.v2.billing.meterEventStream.create({
          events: [{
            event_name: config.billing.meters.output,
            payload: {
              stripe_customer_id: config.billing.customer,
              value: completionTokens.toString(),
            }
          }]
        });
      }
    }
  };
  return {
    wrapGenerate: async ({doGenerate}) => {
      const result = await doGenerate();
      if (config.billing) {
        await bill(result.usage);
      }
      return result;
    },
    wrapStream: async ({doStream}) => {
      const {stream, ...rest} = await doStream();
      const transformStream = new TransformStream<
        LanguageModelV1StreamPart,
        LanguageModelV1StreamPart
      >({
        async transform(chunk, controller) {
          if (chunk.type === 'finish') {
            if (config.billing) {
              await bill(chunk.usage);
            }
          }
          controller.enqueue(chunk);
        },
      });
      return {
        stream: stream.pipeThrough(transformStream),
        ...rest,
      };
    },
  };
}
このコードは、AIモデルの入力(プロンプト)トークンと出力(コンプレーション)トークンの両方を計測し、Stripe Meter EventStream APIを使用して記録します。
あとはこれをwrapLanguageModel()のmiddlewareに渡すだけです。
const myStripeToolKit = createStripeUsageBasedBillingMiddleware({
  secretKey: STRIPE_SECRET_API_KEY,
  billing: {
    customer: STRIPE_CUSTOMER_ID,
    meters: {
      input: STRIPE_METER_NAME_INPUT,
      output: STRIPE_METER_NAME_OUTPUT
    }
  }
})
const model = wrapLanguageModel({
  model: createAnthropic({
    apiKey: CLAUDE_API_KEY
  })('claude-3-5-sonnet-20241022'),
  middleware: [myStripeToolKit],
})
Meter EventStream APIのリクエストログは、Stripeワークベンチのリクエストログに残らないことに注意しましょう。大量のAPIリクエストが送信される前提のため、意図的にログに残らない作りとなっています。
独自の計測middlewareのコード解説
先ほど実装したmiddlewareコードの主要部分を詳しく解説します。
1. 設定オブジェクトの構造
type StripeUsageBasedBillingMiddlewareConfig = {
  secretKey: string
  billing?: {
    type?: 'token';
    customer: string;
    meters: {
      input?: string;
      output?: string;
    };
  };
};
このタイプ定義では:
secretKey: Stripeの認証に必要なシークレットキーbilling: 課金設定を含むオプショナルなオブジェクトtype: 現在は’token’のみサポート(将来の拡張性を考慮)customer: 計測対象のStripeカスタマーIDmeters: 入力と出力のメーター名を指定するオブジェクト
2. 課金処理関数
const bill = async ({
  promptTokens,
  completionTokens,
}: {
  promptTokens: number;
  completionTokens: number;
}) => {
  const meterEventSession = await new Stripe(config.secretKey, {
    apiVersion: '2025-01-27.acacia'
  }).v2.billing.meterEventSession.create();
  const stripe = new Stripe(meterEventSession.authentication_token)
  
  if (config.billing) {
    if (config.billing.meters.input) {
      await stripe.v2.billing.meterEventStream.create({
        events: [{
          event_name: config.billing.meters.input,
          payload: {
            stripe_customer_id: config.billing.customer,
            value: promptTokens.toString(),
          }
        }]
      });
    }
    // 出力トークンの処理も同様...
  }
};
このbill関数は:
- 新しいMeter EventSessionを作成して認証トークンを取得
 - その認証トークンを使って新しいStripeインスタンスを初期化
 - 入力トークンと出力トークンの両方について、設定されていれば対応するメーターにイベントを送信
 
重要なポイントは、valueの値を文字列に変換していることです。Stripe APIは数値そのものではなく、文字列形式での数値を期待しています。
3. middlewareオブジェクトの構成
return {
  wrapGenerate: async ({doGenerate}) => {
    const result = await doGenerate();
    if (config.billing) {
      await bill(result.usage);
    }
    return result;
  },
  wrapStream: async ({doStream}) => {
    const {stream, ...rest} = await doStream();
    const transformStream = new TransformStream<
      LanguageModelV1StreamPart,
      LanguageModelV1StreamPart
    >({
      async transform(chunk, controller) {
        if (chunk.type === 'finish') {
          if (config.billing) {
            await bill(chunk.usage);
          }
        }
        controller.enqueue(chunk);
      },
    });
    return {
      stream: stream.pipeThrough(transformStream),
      ...rest,
    };
  },
};
Vercel AI SDKのミドルウェアには2つの重要な関数があります:
wrapGenerate
非ストリーミングモードでAIモデルを使用する場合に呼び出される関数です。AIからの応答が完全に生成された後、usage情報を含む結果オブジェクトを使用して課金処理を行います。
wrapStream
ストリーミングモードでAIモデルを使用する場合に呼び出される関数です。TransformStreamを使用して応答ストリームを変換します。ストリームの最後(chunk.type === 'finish')に到達したときにのみ課金処理を行い、それまではチャンクをそのまま通過させます。
このアプローチにより、ストリーミングモードでもノンストリーミングモードでも同じ課金ロジックを適用できます。特にストリーミングモードでは、ユーザーエクスペリエンスを損なうことなく、全てのトークンが生成された後に最終的な使用量に基づいて課金できる点が優れています。
実際のユースケース例
この実装は以下のようなユースケースで特に有効です:
1. SaaS型AIアシスタントサービス
ユーザーごとに異なる利用プランを提供し、トークン使用量に基づいて課金するSaaSサービスでは、高速かつ信頼性の高い利用量計測が必須です。V2 APIの高いスループットは、多数のユーザーが同時にAIを使用するシナリオで特に威力を発揮します。
2. エンタープライズ向けAIソリューション
大企業向けにカスタマイズされたAIソリューションでは、部門ごとや用途ごとに利用量を正確に追跡する必要があります。Stripe V2 APIを使用することで、大量のAPI呼び出しを効率的に処理しながら、詳細な利用統計を記録できます。
3. マルチテナントAIプラットフォーム
複数の企業や組織にAIサービスを提供するプラットフォームでは、テナントごとの正確な利用量計測が重要です。V2 APIの高いレート制限により、サービスのスケーラビリティを確保しながら、正確な利用量ベースの課金を実現できます。
トラブルシューティングと注意点
認証トークンの有効期限
Meter EventStream APIの認証トークンには15分の有効期限があります。トークンの期限切れを適切に処理するロジックを実装しないと、「認証エラー」が発生することがあります。前述のセッション管理コードを参考に、有効期限の少し前(例:残り5分)にトークンを更新する仕組みを実装しましょう。
エラーハンドリング
高トラフィック環境では、一時的なネットワークエラーなどが発生する可能性があります。以下のように、リトライロジックを実装することをお勧めします:
async function sendMeterEvent(stripe, eventData, retries = 3) {
  try {
    return await stripe.v2.billing.meterEventStream.create(eventData);
  } catch (error) {
    if (retries > 0 && error.type === 'api_connection_error') {
      // 接続エラーの場合は少し待ってリトライ
      await new Promise(resolve => setTimeout(resolve, 500));
      return sendMeterEvent(stripe, eventData, retries - 1);
    }
    console.error('Failed to send meter event after retries:', error);
    // エラーログを送信するなどの追加処理
    throw error;
  }
}
バッチ処理の最適化
多数のイベントを一度に送信する場合は、単一のイベントを送信するよりも、複数のイベントをバッチ処理することで効率が向上します:
await stripe.v2.billing.meterEventStream.create({
  events: [
    {
      event_name: config.billing.meters.input,
      payload: { ... }
    },
    {
      event_name: config.billing.meters.output,
      payload: { ... }
    }
    // 最大100イベントまで一度に送信可能
  ]
});
まとめと発展的な使い方
Stripe V2 APIを使用したAI利用量計測の実装について解説しました。この実装により、従来の10倍のスループットで利用量データを送信できるようになり、大規模なAIサービスの運用が可能になります。
今回紹介した方法をベースに、さらに発展させられる方向性としては:
- 分析基盤との連携: Stripeの利用量データをBigQueryやRedshiftなどのデータウェアハウスと連携させ、詳細な利用分析を行う
 - リアルタイムダッシュボード: 管理者向けに、リアルタイムでAI利用量を可視化するダッシュボードを構築する
 - プリペイド方式の実装: 事前にトークンを購入するプリペイド方式と組み合わせ、利用量に応じてトークン残高を減算する仕組みを構築する
 - 複数AIモデルの統合管理: 複数のAIプロバイダー(OpenAI、Anthropic、Googleなど)を統合的に管理し、モデルごとの利用量と費用を一元的に把握できるシステムを構築する
 
Stripe V2 APIの高いスループットを活用することで、より大規模で複雑なAIシステムの課金管理が可能になります。特に、同時接続数の多いサービスや、トークン単位の精緻な課金が必要なビジネスモデルにおいて、今回の実装方法は大きな価値を発揮するでしょう。
最後に、Stripe V2 APIは比較的新しいAPIですので、今後も改良される可能性があります。定期的にStripeのドキュメントをチェックして、最新の機能や改善点を取り入れていくことをお勧めします。