Stripe Agent Toolkit で Vercel AI SDK のトークン使用量を自動計測する

Stripe Agent Toolkitを使って、Vercel AI SDKのトークン使用量を自動計測し課金する方法を解説。ミドルウェアとして簡単に実装でき、OpenAIやClaudeのAPIと連携した従量課金モデルを実現する具体的な手順を紹介します。

広告ここから
広告ここまで

目次

    最近、AI アプリケーションの従量課金を実装する機会が増えてきました。特に LLM を使ったサービスでは、トークン数に基づいた課金モデルが一般的になっています。今回は Stripe Agent Toolkit と Vercel AI SDK を組み合わせて、トークン使用量の自動計測と課金を実現する方法を紹介します。

    実装してみると意外とシンプルで、middleware として差し込むだけで動作するのが魅力的でした。ただし、いくつかハマりポイントもあったので、その解決方法も含めて解説していきます。

    前提

    AI SDK のクイックスタートに基づいて Next.js App Router アプリをセットアップしていることを前提とします。

    OpenAI または Claude の API キーを環境変数に設定済みであることも必要になります。

    Stripe で従量課金のサブスクリプションと計測イベントを作成する

    Stripe の Docs を参考にサブスクリプションなどのリソースを作りましょう。基本的にはドキュメントの通りに進めれば問題ありません。

    まず計測用のメーターを作成します。

    stripe billing meters create  \
      --display-name="Alpaca AI tokens" \
      --event-name=alpaca_ai_tokens \
      -d "default_aggregation[formula]"=sum \
      -d "customer_mapping[event_payload_key]"=stripe_customer_id \
      -d "customer_mapping[type]"=by_id \
      -d "value_settings[event_payload_key]"=value

    次に、作成したメーターを利用する料金プランと商品を作成しましょう。

    stripe prices create  \
      --currency=usd \
      --unit-amount=4 \
      --billing-scheme=per_unit \
      -d "transform_quantity[divide_by]"=1000 \
      -d "transform_quantity[round]"=up \
      -d "recurring[usage_type]"=metered \
      -d "recurring[interval]"=month \
      -d "recurring[meter]"={{METER_ID}} \
      -d "product_data[name]"="Alpaca AI tokens"

    顧客の作成とテスト用の決済情報の登録、そしてサブスクリプションの作成については、ダッシュボードから済ませる方が簡単かもしれません。

    Stripe ダッシュボード

    環境変数として以下の値を保存しておきます。

    STRIPE_METER_ID=mtr_test_xxxx
    STRIPE_CUSTOMER_ID=cus_xxxx

    npm run cf-typegen を実行します。

    % npm run cf-typegen
    
    Generating project types...
    
    interface CloudflareEnv {
            STRIPE_METER_ID: string;
            STRIPE_CUSTOMER_ID: string;
    }

    Agent SDK をインストールする

    npm install @stripe/agent-toolkit

    .dev.vars ファイルに以下を追加しましょう。

    STRIPE_SECRET_API_KEY=sk_test_5xxxx

    再度 npm run cf-typegen を実行します。

    % npm run cf-typegen
    
    Generating project types...
    
    interface CloudflareEnv {
            STRIPE_SECRET_API_KEY: string;
    }

    AI Chat API に middleware を追加する

    Vercel AI のチャット API は以下のような構成になっています。デフォルトでは POST /api/chat として作成されます。

    import { getRequestContext } from "@cloudflare/next-on-pages";
    import { CoreMessage, streamText } from 'ai';
    import { createAnthropic } from '@ai-sdk/anthropic';
    
    export const runtime = 'edge'
    
    export async function POST(req: Request) {
      const { messages } = await req.json<{messages: CoreMessage[]}>();
      const { CLAUDE_API_KEY } = getRequestContext().env
      const result = streamText({
        model: createAnthropic({
            apiKey: CLAUDE_API_KEY
        })('claude-3-5-sonnet-20241022'),
        messages,
      })
      return result.toDataStreamResponse();
    }

    Stripe のミドルウェアは次のように使用できます。

    import {StripeAgentToolkit} from '@stripe/agent-toolkit/ai-sdk';
    import {openai} from '@ai-sdk/openai';
    import {
      generateText,
      experimental_wrapLanguageModel as wrapLanguageModel,
    } from 'ai';
    
    const stripeAgentToolkit = new StripeAgentToolkit({
      secretKey: process.env.STRIPE_SECRET_KEY!,
      configuration: {
        actions: {
          paymentLinks: {
            create: true,
          },
        },
      },
    });
    
    const model = wrapLanguageModel({
      model: openai('gpt-4o'),
      middleware: stripeAgentToolkit.middleware({
        billing: {
          customer: 'cus_123',
          meters: {
            input: 'input_tokens',
            output: 'output_tokens',
          },
        },
      }),
    });

    これらを組み合わせると以下のようになります。

    import { getRequestContext } from "@cloudflare/next-on-pages";
    import { CoreMessage, streamText, wrapLanguageModel } from 'ai';
    import { StripeAgentToolkit } from '@stripe/agent-toolkit/ai-sdk';
    import { yourLogMiddleware } from "@/app/debug-middleware";
    import { createAnthropic } from '@ai-sdk/anthropic';
    
    export const runtime = 'edge'
    
    export async function POST(req: Request) {
      const { messages } = await req.json<{messages: CoreMessage[]}>();
      const { STRIPE_SECRET_API_KEY, STRIPE_CUSTOMER_ID, STRIPE_METER_NAME_INPUT, STRIPE_METER_NAME_OUTPUT, CLAUDE_API_KEY } = getRequestContext().env
      const stripeAgentToolkit = new StripeAgentToolkit({
        secretKey: STRIPE_SECRET_API_KEY,
        configuration: {
          actions: {
            paymentLinks: {
              create: true,
            },
          },
        },
      });
      const model = wrapLanguageModel({
        model: createAnthropic({
            apiKey: CLAUDE_API_KEY
        })('claude-3-5-sonnet-20241022'),
        middleware: [stripeAgentToolkit.middleware({
            billing: {
                customer:STRIPE_CUSTOMER_ID,
                meters: {
                    input: STRIPE_METER_NAME_INPUT,
                    output: STRIPE_METER_NAME_OUTPUT
                }
            }
        }), yourLogMiddleware]
      })
      const result = streamText({
        model,
        messages,
      })
      return result.toDataStreamResponse();
    }

    動作をテストする

    実際に AI チャットと数ターン会話してみましょう。

    チャット画面

    Stripe ダッシュボードで設定したメーターを確認すると、イベントの呼び出し記録がリアルタイムで表示されているはずです。

    メーターの記録

    サブスクリプションにも消費トークン数に応じた数量が追加されているのが確認できるでしょう。

    サブスクリプションの数量

    トラブルシューティング

    Stripe API がエラーを出す場合

    チャットの API がエラーになることがあります。以下はエラーの例です。

     ⨯ [Error: failed to pipe response] {
      [cause]: [Error: No active meter found for event name "mtr_test_xxx".] {
        type: 'StripeInvalidRequestError',
        raw: {
          message: 'No active meter found for event name "mtr_test_61RO68cq6jHaKcIIH41IDF6qBhtttNwm".',
          request_log_url: 'https://dashboard.stripe.com/test/logs/req_72TbA2TrxVIq5u?t=1740102994',
          type: 'invalid_request_error',
          headers: [Object],
          statusCode: 400,

    stripeAgentToolkit.middleware に渡すのは Meter ID ではなく Meter Name である点に注意してください。

    どれだけ使っても請求額が0円のままになる場合

    このケースでは、まず Stripe ダッシュボードを開いてワークベンチからリクエストログを調べることが重要です。

    ワークベンチ

    /v1/billing/meter_events の POST 本文のリクエストが 0 になっていないかチェックしましょう。

    リクエスト詳細

    もし payload.value"0" であれば、使用している LLM の API がトークン数を返していないか、Vercel の AI SDK が拾えていない可能性があります。Stream で表示する場合、以下のような middleware でデバッグできるでしょう。

    import type { LanguageModelV1Middleware, LanguageModelV1StreamPart } from 'ai';
    
    export const debugMiddleware: LanguageModelV1Middleware = {
      wrapGenerate: async ({ doGenerate, params }) => {
        console.log('doGenerate called');
    
        const result = await doGenerate();
    
        console.log('doGenerate finished');
    
        return result;
      },
    
      wrapStream: async ({ doStream }) => {
        console.log('doStream called');
    
        const { stream, ...rest } = await doStream();
    
        const transformStream = new TransformStream<
          LanguageModelV1StreamPart,
          LanguageModelV1StreamPart
        >({
          transform(chunk, controller) {
            if (chunk.type === 'finish') {
                console.log({usage: chunk.usage});
            }
    
            controller.enqueue(chunk);
          },
    
          flush() {
            console.log('doStream finished');
          },
        });
    
        return {
          stream: stream.pipeThrough(transformStream),
          ...rest,
        };
      },
    };

    これを wrapLanguageModel に追加します。middleware は配列も受け付けるため、配列にして追加できます。

      const model = wrapLanguageModel({
        model: workersai('@cf/deepseek-ai/deepseek-r1-distill-qwen-32b'),
        middleware: [stripeAgentToolkit.middleware({
            billing: {
                customer:STRIPE_CUSTOMER_ID,
                meters: {
                    input: STRIPE_METER_NAME_INPUT,
                    output: STRIPE_METER_NAME_OUTPUT
                }
            }
        }), debugMiddleware]
      })

    ログをチェックして、トークン数が 0 の場合はモデルの変更を検討することになります。

    {
      usage: {
      type: 'finish',
      finishReason: 'stop',
      usage: { promptTokens: 0, completionTokens: 0 }
    }

    正しく計測できている場合、以下のようなログが出力されます。

    doStream called
    { usage: { promptTokens: 12, completionTokens: 21 } }
    doStream finished

    私が試してダメだったモデルを参考までに記載しておきます。

    • workersai('@cf/deepseek-ai/deepseek-r1-distill-qwen-32b')
    • workersai('@cf/meta/llama-3.1-70b-instruct')

    確実なのは、Stripe Docs のクイックスタートに記載されている 4 種類を使うことでしょう。OpenAI、Anthropic、Llama、Mistral が対応しています。

    @cf/meta/llama-xx がダメだったため、workers-ai-provider が対応していない可能性もあります。確実に進めるには、各プロバイダーの SDK を使う方法が安全かもしれません。Amazon Bedrock については動作未確認です。

    Vercel AI SDK と組み合わせるメリット

    集計結果を送信する API リクエストの数を絞りやすいことと、middleware という形でメインの LLM ロジックから切り離した実装をできることがメリットではないでしょうか。SDK の実装を見ると、Stream でレスポンスを返す場合、全てのチャンクを返し切ってから 1 回だけ利用量を送信するリクエストを送信しています。

    リクエストの流れ

    chunk 内容を見る middleware を差し込んでみると、すべてのテキスト生成が完了してから SDK がトークン数を middleware に渡していることもわかります。文字数を数えるなどの方法を行う場合、どうしても chunk 毎にリクエストを送信するか、文字数を一時的に記録しておく場所が必要になるでしょう。しかしこの方法であれば、最後に集計結果に基づいたリクエストを 1 度投げるだけで済みます。

    { type: 'text-delta', textDelta: '\nビジネスの規模や形' }
    { type: 'text-delta', textDelta: '態に応じて' }
    { type: 'text-delta', textDelta: '、最適な機能を選択し' }
    { type: 'text-delta', textDelta: '活用することで、効率的な' }
    { type: 'text-delta', textDelta: '運営が可能になります。ま' }
    { type: 'text-delta', textDelta: 'ずは無料のテ' }
    { type: 'text-delta', textDelta: 'スト環境で機能を確認' }
    { type: 'text-delta', textDelta: 'することをお勧めします。' }
    {
      type: 'finish',
      finishReason: 'stop',
      usage: { promptTokens: 1184, completionTokens: 722 },
      providerMetadata: { anthropic: { cacheCreationInputTokens: 0, cacheReadInputTokens: 0 } }
    }

    Next step

    トークン数や利用量に応じた請求が簡単にできるのが Stripe Agent Toolkit の良いところでしょう。Vercel AI SDK と組み合わせることで、入出力それぞれのトークン数に対して請求のためのメーターを仕込めます。実際のアプリケーションでは、stripeAgentToolkit.middlewarebilling.customer に現在ログイン中のユーザーと対応した Stripe Customer ID を設定することになります。この辺りは Clerk や Supabase / Okta CIC などの連携になるため、ぜひネクストステップとして挑戦してみてください。

    この呼び出し方で API レートリミットは大丈夫か

    Stripe Docs によると、本番環境では秒間 1,000 件・1 顧客辺り 1 件の同時呼び出しが許可されています。そのため、よほどの集中アクセスがなければ、この使い方でも問題なさそうです。

    Meter Event (メーターイベント) エンドポイントでは、本番環境で 1 秒あたり 1,000 件の呼び出しが許可され、メーターごとに 1 顧客あたり 1 件の同時呼び出しが許可されます。サービスがこの制限を超える可能性がある場合は、商品を一定の量に「まとめる」ことができます。たとえば、1,000 件のリクエストごとに請求する場合、商品を「1,000 回の取引単位」にまとめて、1,000 回ごとに 1 つの使用記録を送信できます。

    ちなみに、この SDK はまだ利用していませんが、V2 API では秒間 10,000 件まで耐えるそうです。

    API v2 では、メーターイベントストリームを使用して、毎秒最大 10,000 件のイベントを Stripe に送信できます。これは本番環境でのみ機能します。

    ただしどちらも本番環境のみで、テスト環境では秒間 100 件までという点に注意しましょう。また、Connect プラットフォームについても秒間 100 件までとのことです。

    まとめ

    Stripe Agent Toolkit と Vercel AI SDK を組み合わせることで、トークン使用量の自動計測と課金を簡単に実装できました。特に middleware として実装できる点が優れており、既存のコードへの影響を最小限に抑えながら機能を追加できます。

    実装時の注意点として、Meter ID ではなく Meter Name を使う必要があることや、一部のモデルではトークン数が正しく取得できない可能性があることを覚えておいてください。本番環境への導入前には、必ず使用するモデルでトークン数が正しく計測されることを確認することをお勧めします。

    今後は V2 API の活用や、より高度な課金モデルの実装なども視野に入れて、AI アプリケーションの収益化を進めていけるのではないでしょうか。

    参考資料

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark