AI

GLM 5.2×Vercel AI Gateway入門——1MコンテキストをNext.jsで使う

Vercel AI Gatewayに追加されたGLM 5.2を題材に、Next.js App Router、Firestore、GitHub Actionsで長文コンテキストAI機能を安全に試す方法を解説します。

2026年6月22日
Vercel AI GatewayGLM 5.2Next.jsFirestoreAI SDK
GLM 5.2×Vercel AI Gateway入門——1MコンテキストをNext.jsで使う

はじめに

AIモデルを業務アプリに組み込むとき、こんな悩みはありませんか?

  • 長い仕様書、チケット一覧、変更履歴を1回のレビューに入れたい
  • モデルを切り替えるたびにSDKや認証方式が変わってしまう
  • AI利用量を後から追えるように、Firestoreへ監査ログを残したい

2026年6月16日、Vercelは GLM 5.2がAI Gatewayで利用可能になった と発表しました。公式Changelogでは、GLM 5.2は長時間タスクやプロジェクトレベルのエンジニアリング文脈に向いたモデルとして紹介され、コンテキストウィンドウはGLM 5.1の200Kから1Mトークンへ拡張されたと説明されています。AI SDKで使うモデルIDは zai/glm-5.2 です。

本記事では、架空の業務改善SaaS「ReviewPort」を題材に、Next.js App Router + TypeScript + Firestoreで、複数チケットをまとめてAIレビューする機能を作ります。

参考にした一次情報は次の通りです。

何が変わるのか

GLM 5.2の注目点は「1Mトークンまで入るから何でも丸ごと投げる」ではありません。実務では、AI Gatewayを挟み、モデル呼び出し、利用量、予算、認証の扱いをアプリ側から分離しやすくなる点が重要です。

観点直接プロバイダを呼ぶ場合AI Gateway経由で考える場合
モデル指定SDKやプロバイダごとに差が出るAI SDKでは文字列モデルIDで指定できる
認証各プロバイダのAPIキー管理が中心VercelデプロイではOIDC認証を使える
利用量管理アプリ側で集計が必要Gateway側の利用監視や予算設定を併用できる

AI SDKの公式ドキュメントでは、streamText は言語モデルからのテキスト生成をストリーミングする関数として説明されています。Vercelのモデル・プロバイダドキュメントでは、モデルを文字列で指定した場合、AI SDKのデフォルトプロバイダとしてVercel AI Gatewayが使われると説明されています。

事前準備

ReviewPortでは、AI SDKとFirebase Admin SDKを使います。

yarn add ai firebase-admin

Vercel上のProduction / Previewデプロイでは、AI Gateway providerがOIDCトークンによる認証をサポートします。公式ドキュメントでは、Vercelデプロイ上ではOIDC認証が自動的に扱われ、ローカル開発ではVercel CLIで認証し、vercel env pullvercel dev を使う流れが案内されています。

vercel link
vercel env pull
vercel dev

APIキーを明示的に渡した場合は、そのAPIキーが優先されます。チームで検証するときは「ローカルはAPIキー、本番はOIDC」のように混ぜるより、環境ごとの認証方針をREADMEや運用メモに書いておくとレビューしやすくなります。

ハンズオン1: Firestoreの台帳を決める

まず、AIリクエストをFirestoreに残す台帳を作ります。プロンプト全文を保存すると機密情報や個人情報を抱えやすいため、ここでは入力件数、対象チケット、モデルID、トークン利用量、終了理由を中心に残します。

レビュー対象のチケットは、projectTickets コレクションに保存されている想定です。架空のチケット番号は RP-001 から RP-030 までとし、実際の業務データは使いません。

ハンズオン2: Route HandlerでGLM 5.2を呼ぶ

次に、Route Handlerを作ります。requireUserassertCanReadProject は、アプリ側で用意する認証・認可ヘルパーです。AI Gatewayはアプリの業務認可を代行しないため、モデル呼び出しより前に必ず権限を確認します。

// src/app/api/projects/[projectId]/ai-review/route.ts
import { streamText } from "ai";
import { FieldValue } from "firebase-admin/firestore";
import { adminDb } from "@/lib/firebase/admin";
import { requireUser } from "@/lib/auth/requireUser";
import { assertCanReadProject } from "@/lib/projects/permissions";

type Params = {
  params: Promise<{ projectId: string }>;
};

type RequestBody = {
  ticketIds: string[];
  question: string;
};

export async function POST(request: Request, { params }: Params) {
  const user = await requireUser(request);
  const { projectId } = await params;
  const body = (await request.json()) as RequestBody;

  await assertCanReadProject(user.uid, projectId);

  const ticketIds = body.ticketIds.slice(0, 30);
  const question = body.question.trim().slice(0, 1000);

  if (ticketIds.length === 0 || question.length === 0) {
    return Response.json({ error: "Invalid request" }, { status: 400 });
  }

  const ticketsSnapshot = await adminDb
    .collection("projectTickets")
    .where("projectId", "==", projectId)
    .where("ticketId", "in", ticketIds)
    .get();

  const ticketContext = ticketsSnapshot.docs
    .map((doc) => {
      const data = doc.data() as {
        ticketId: string;
        title: string;
        status: string;
      };

      return `${data.ticketId}: ${data.title} (${data.status})`;
    })
    .join("\n\n---\n\n");

  const requestRef = adminDb.collection("aiReviewRequests").doc();

  await requestRef.set({
    projectId,
    requestedBy: user.uid,
    ticketIds,
    model: "zai/glm-5.2",
    status: "streaming",
    inputTokens: null,
    outputTokens: null,
    totalTokens: null,
    finishReason: null,
    createdAt: FieldValue.serverTimestamp(),
  });

  const result = streamText({
    model: "zai/glm-5.2",
    system:
      "You are a senior TypeScript reviewer. Reply in Japanese. Focus on missing requirements, risk, and next actions.",
    prompt: [
      "次のチケット群をレビューしてください。",
      "Next.js App Router、Firestore、GitHub Actionsで実装する前提です。",
      `質問: ${question}`,
      "チケット一覧:",
      ticketContext,
    ].join("\n\n"),
    onFinish: async ({ usage, finishReason, response }) => {
      await requestRef.update({
        status: "done",
        responseModelId: response.modelId,
        finishReason,
        inputTokens: usage.inputTokens ?? null,
        outputTokens: usage.outputTokens ?? null,
        totalTokens: usage.totalTokens ?? null,
        finishedAt: FieldValue.serverTimestamp(),
      });
    },
  });

  return result.toTextStreamResponse({
    headers: {
      "x-ai-review-request-id": requestRef.id,
    },
  });
}

ここでは30件までに制限しています。1Mコンテキストがあっても、Firestore上の全履歴を無条件に詰め込む設計は避けます。まず関連チケット、最新の受け入れ条件、直近レビューの要約を選び、必要なときだけ追加で読ませる構成にすると、費用と情報漏えい範囲を抑えやすくなります。

ハンズオン3: UIからストリームを読む

Route Handlerはプレーンテキストのストリームを返します。最小構成では、Client Componentから fetch し、ReadableStream を順番に読みます。

"use client";

import { useState } from "react";

export function AiReviewPanel({ projectId, ticketIds }: {
  projectId: string;
  ticketIds: string[];
}) {
  const [answer, setAnswer] = useState("");

  async function runReview() {
    setAnswer("");
    const response = await fetch(`/api/projects/${projectId}/ai-review`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        ticketIds,
        question: "実装前に潰すべきリスクを優先度順に整理してください。",
      }),
    });

    if (!response.body) {
      return;
    }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      setAnswer((current) => current + decoder.decode(value));
    }
  }

  return (
    <section className="space-y-3">
      <button type="button" onClick={runReview}>
        AIレビューを実行
      </button>
      <pre className="whitespace-pre-wrap">{answer}</pre>
    </section>
  );
}

本番では、エラー表示、キャンセル、再実行、利用量表示を加えます。まずはRoute Handlerの境界を薄く作り、モデル呼び出しとFirestore台帳が正しく動くかを検証するのが現実的です。

GitHub Actionsで設定ミスを防ぐ

最後に、CIで最低限の逆戻りを防ぎます。ここでは、ビルドに加えてモデルIDが消えていないかを確認します。APIキーをCIに渡して実推論まで走らせるかは、予算とセキュリティ方針に合わせて別ジョブに分けます。

name: ai-review-ci

on:
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "yarn"
      - run: yarn install --frozen-lockfile
      - run: yarn build
      - name: Check GLM 5.2 route
        run: grep -R "zai/glm-5.2" src/app/api/projects

AI機能のCIでは、毎回モデルを呼ぶより先に、設定ファイル、モデルID、認証情報の渡し方を確認するだけでも効果があります。実推論のスモークテストは、手動実行、夜間実行、または小さな固定プロンプトに絞ると費用を管理しやすくなります。

まとめ

GLM 5.2の1Mコンテキストは、チケット群、設計メモ、受け入れ条件をまとめて扱いたいWeb系エンジニアにとって魅力的です。ただし、長い入力をそのまま投げるより、Next.js側で権限確認と入力制限を行い、Firestoreに利用ログを残し、AI Gatewayでモデル呼び出しの運用面を吸収する構成が実務向きです。

今回の要点は次の3つです。

  • GLM 5.2はVercel AI Gatewayで zai/glm-5.2 として利用できます
  • VercelデプロイではAI Gateway providerのOIDC認証を使えるため、長期APIキーを減らせます
  • 1Mコンテキストでも、Firestoreから必要な文脈だけを選び、利用量と終了理由を監査ログに残す設計が重要です

まずは30件程度の架空チケットレビューから始め、回答品質、利用量、レビュー時間をFirestoreで見える化してから、本番の業務フローへ広げるのがおすすめです。