プログラミング

OpenAI・Claude・Geminiを切り替えて使う!生成AI API活用の共通ラッパー設計(価格比較、TypeScript実装付き)

現在、主流となっている生成AI APIといえば OpenAIClaude(Anthropic)Gemini(Google) の3つが代表格です。
これらのAPIを使い分けたり、用途に応じて切り替えられる仕組みがあると、開発や検証が非常に効率化します。

今回は、複数の生成AI APIを一つの関数で切り替えて使える共通ラッパーの作成方法を紹介します。
さらに、対象ページの情報を取得してJSON形式に直してデータを取得する処理まで実装していきます。

各APIの価格比較

生成AIを活用したアプリケーション開発や業務改善が進む中、主要なAI APIとして注目されているのが「OpenAI(ChatGPT)」「Anthropic Claude」「Google Gemini」かと思います。本章では各APIの特徴等の紹介は省略しますが、

コスト面だけで見るとGeminiが圧倒的に安いです。

現状、利用範囲内において各AIの精度にあまり違いを感じられなかった私はコスト面重視でgeminiを利用することが多いです。

以下コストの比較表だけでもおいておきます。(2025年5月時点)

モデル/API入力(1Mトークン)出力(1Mトークン)
OpenAI GPT-3.5 Turbo$0.50$1.50
OpenAI GPT-4o$2.5 *$10
Claude 3.5 Haiku$0.80 *$4
Claude 3.7 Sonnet$3 *$15
Gemini 1.5 Flash$0.075$0.30
Gemini 2.5 Flash $0.15思考あり$0.6
思考なし$3.5

またgeminiは無料枠であればレート制限ありますが、1日1500回まで利用可能です。

※ OpenAIやClaudeは特定モデルはPrompt cachingの利用で安くなる可能有ります。

共通ラッパーの構成と使い方

以下のようにsrc/ai/配下にファイルを分けて管理します。

src/ai/
├── index.ts       // API選択ラッパー
├── openai.ts      // OpenAI用処理
├── claude.ts      // Claude用処理
└── gemini.ts      // Gemini用処理

index.ts(共通ラッパー)

import { ZodTypeAny } from "zod";
import { fetchParsedClaude } from "./claude";
import { fetchParsedGemini } from "./gemini";
import { fetchParsedOpenAiGPT4o } from "./openai";

// ---- 型定義 ---- //
export type Option = {
  max_tokens?: number;
};

type OpenAIArgs = {
  ai: "openai";
  messages: any[];
  schema: ZodTypeAny;
  option?: Option;
};

type ClaudeArgs = {
  ai: "claude";
  messages: any[];
  schema: ZodTypeAny;
  option?: Option;
};

type GeminiArgs = {
  ai: "gemini";
  messages: any[];
  schema: ZodTypeAny;
  option?: Option;
};

type AIArgs = OpenAIArgs | ClaudeArgs | GeminiArgs;

// ---- 関数オーバーロード(オプション) ---- //
export function fetchParsedWithAi(args: OpenAIArgs): Promise<any>;
export function fetchParsedWithAi(args: ClaudeArgs): Promise<any>;
export function fetchParsedWithAi(args: GeminiArgs): Promise<any>;

// ---- 実装 ---- //
export async function fetchParsedWithAi(args: AIArgs): Promise<any> {
  if (args.ai === "openai") {
    return fetchParsedOpenAiGPT4o(args.messages, args.schema, args.option);
  } else if (args.ai === "claude") {
    return fetchParsedClaude(args.messages, args.schema, args.option);
  } else if (args.ai === "gemini") {
    return fetchParsedGemini(args.messages, args.schema, args.option);
  } else {
    throw new Error("Unknown AI type");
  }
}

各AIの実装例

以下各処理のprocess.envで設定するAPIキーは各AIのサイトからアカウント作成して取得しておきましょう!

Claude (Anthropic Claude 3)

import Anthropic from "@anthropic-ai/sdk";
import { ZodArray, ZodTypeAny } from "zod";
import { Option } from "./";

export const fetchParsedClaude = async (
  messages: any[],
  schema: ZodTypeAny,
  option?: Option
) => {
  const anthropic = new Anthropic({
    apiKey: process.env.CLAUDE_API_KEY,
  });
  const completion = await anthropic.messages.create({
    model: "claude-3-7-sonnet-20250219", // モデルは適宜修正
    max_tokens: 1024,
    temperature: 0,
    messages: messages,
    ...option,
  });

  const block = completion.content.find((c) => c.type === "text") as
    | { type: "text"; text: string }
    | undefined;

  if (!block) {
    throw new Error("Claudeの返答にtextタイプのブロックが含まれていません");
  }

  const raw = block.text.trim();

  if (!raw) {
    throw new Error("Claudeの返答が空です");
  }

  // schemaが配列型か判定
  const expectsArray = schema instanceof ZodArray;

  let firstBraceIndex: number;
  let lastBraceIndex: number;

  if (expectsArray) {
    firstBraceIndex = raw.indexOf("[");
    lastBraceIndex = raw.lastIndexOf("]");
  } else {
    firstBraceIndex = raw.indexOf("{");
    lastBraceIndex = raw.lastIndexOf("}");
  }

  if (firstBraceIndex === -1 || lastBraceIndex === -1) {
    throw new Error("期待する形式のJSON部分が見つかりませんでした");
  }

  const jsonString = raw.slice(firstBraceIndex, lastBraceIndex + 1);

  let parsed;
  try {
    parsed = JSON.parse(jsonString);
  } catch (e) {
    console.error("JSONパース失敗:\n", jsonString);
    throw e;
  }

  return schema.parse(parsed);
};

OpenAI (GPT-4o)

import OpenAI from "openai";
import { zodResponseFormat } from "openai/helpers/zod";
import { z, ZodArray, ZodTypeAny } from "zod";
import { Option } from "./";

export const fetchParsedOpenAiGPT4o = async (
  messages: any[],
  schema: ZodTypeAny,
  option?: Option
) => {
  const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
  });

  // formatNameがなければ"default"を使う
  const finalFormatName = "data";

  const isArraySchema = schema instanceof ZodArray;

  // 配列スキーマならラップ
  const fixedSchema = isArraySchema
    ? z.object({ [finalFormatName]: schema })
    : schema;

  const completion = await openai.beta.chat.completions.parse({
    model: "gpt-4o-2024-08-06", // モデルは適宜修正
    messages,
    response_format: zodResponseFormat(fixedSchema, finalFormatName),
    ...option,
  });

  const parsed = completion.choices[0]?.message?.parsed;

  if (!parsed) {
    throw new Error("OpenAIの返答が空です");
  }

  // ★ 配列スキーマだったら unwrap して中身だけ返す
  return isArraySchema ? parsed[finalFormatName] : parsed;
};

Gemini (Google)

import OpenAI from "openai";
import { ZodArray, ZodTypeAny } from "zod";
import { Option } from "./";

export const fetchParsedGemini = async (
  messages: any[],
  schema: ZodTypeAny,
  option?: Option
) => {
  const openai = new OpenAI({
    apiKey: process.env.GEMINI_API_KEY,
    baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
  });

  const response = await openai.chat.completions.create({
    model: "gemini-2.0-flash", // モデルは適宜修正
    messages,
    temperature: 0,
    max_tokens: 1024,
    ...option,
  });

  const raw = response.choices[0]?.message?.content?.trim();

  if (!raw) {
    throw new Error("Geminiの返答が空です");
  }

  // schemaが配列型か判定
  const expectsArray = schema instanceof ZodArray;

  let firstBraceIndex: number;
  let lastBraceIndex: number;

  if (expectsArray) {
    firstBraceIndex = raw.indexOf("[");
    lastBraceIndex = raw.lastIndexOf("]");
  } else {
    firstBraceIndex = raw.indexOf("{");
    lastBraceIndex = raw.lastIndexOf("}");
  }

  if (firstBraceIndex === -1 || lastBraceIndex === -1) {
    throw new Error("期待する形式のJSON部分が見つかりませんでした");
  }

  const jsonString = raw.slice(firstBraceIndex, lastBraceIndex + 1);

  let parsed;
  try {
    parsed = JSON.parse(jsonString);
  } catch (e) {
    console.error("JSONパース失敗:\n", jsonString);
    throw e;
  }

  return schema.parse(parsed);
};

実際の利用例(run.ts)

src/run.tsを作成し、AI呼び出しの処理を作成してみましょう!

// プロンプトの生成(適宜修正)
const buildPrompt = (text: string) => `
以下のテキストを要約して、以下のJSON形式で返してください:

{
  "summary": "<テキストの概要>"
}

<<<
${text}
>>>
`;

// JSONスキーマを定義
const DataSchema = z.object({
  summary: z.string(),
});
type Data = z.infer<typeof DataSchema>;

// メインの処理
async function run() {
  const url = 'https://xxx.example.com'; // ←読み込みたいwebサイトのURL
  const { data } = await axios.get(`https://r.jina.ai/${url}`);
  const prompt = buildPrompt(data);
  const response: Data = await fetchParsedWithAi({
    ai: "openai",  // ←使用したいAIに適宜変更
    messages: [{ role: "user", content: prompt }],
    schema: DataSchema,
  });
  console.log(JSON.stringify(response, null ,2));
}

補足:r.jina.aiとは?

https://r.jina.ai/ は、WebページをLLM向けのプレーンテキストに変換してくれる無料の中継APIです。スクレイピング不要で、対象ページのテキストだけを取得できるため非常に便利です。

おわりに

今回紹介したように、複数の生成AIを簡単に切り替えられるラッパー関数を作っておくと、比較検証や用途に応じた最適なAI選択がスムーズに行えます。

TypeScript × Zod × 各AI SDKを活用した構造化出力のパターン、ぜひプロジェクトに取り入れてみてください。

スポンサーリンク

  • この記事を書いた人
プロフィール画像

かず

バックエンド/フロントエンド/クラウドインフラのフルスタックエンジニア | デジタルノマド(‘23/6〜) で世界放浪中 | 主にプログラミングに役立つ情報を発信 | Xでは旅の様子を発信

-プログラミング
-, , ,