🎉

OpenRouterを導入することで、個人製作規模のアプリで高性能LLMを使用できるようにする考え方

に公開

はじめに:個人開発とLLMの「コストと運用の壁」

「GPT-5やGrok4のような最新のLLMを使って、何か面白いアプリを作りたい」
そう思っても、阻む大きな壁があります。それは、APIの利用料金です。

高性能なLLMは、APIコールごとにコストが発生します。収益バランスを間違えると、API料金はあっという間に膨れ上がり、個人では到底支払えない金額になるかもしれないという不安がありますよね。
かといって、ユーザー毎の課金金額とAPI使用量を管理するのは作業負荷的に現実的ではありません。

この「コストと運用の壁」を華麗に回避し、ユーザーに多種多様なLLMを自由に選んでもらいつつ、開発者はコストリスクを負わない――そんな夢のような方法を実現してくれるのが、OpenRouter です。

本記事では、私が開発した自分が創作した世界を冒険しながら小説が書けるiOSアプリ「NovelADV」の実装例を参考に、個人開発のアプリにOpenRouterを導入し、高性能LLMを活用するための具体的な戦略と実装の考え方を解説します。

OpenRouterとは? LLMの「アグリゲーター」

OpenRouterは、一言で言えば**「LLMのルーター/アグリゲーターサービス」**です。OpenAI、Google、Anthropic、Mistralなど、世界中の様々なLLMプロバイダーのモデルを、単一のAPIエンドポイントを通じて利用できるようにしてくれます。

個人開発者にとって、OpenRouter導入メリットは主に下記です。

  • 💰 開発者のAPIコストがゼロに: ユーザーはOpenRouterに直接クレジットをチャージし、そのクレジットを消費してAPIを利用します。開発者はユーザーのAPIキー(正確にはOAuthで払い出されたキー)を使ってリクエストを代理送信するだけなので、APIコストを一切負担する必要がありません。
  • 🔑 単一のAPIキーとエンドポイント: 複数のLLMプロバイダーと個別に契約し、それぞれのAPI仕様に対応する必要がありません。OpenRouterのAPIキー(またはOAuthフロー)と、OpenAI互換のAPIエンドポイントだけで済みます。
  • 🤖 豊富なモデル選択肢: GPT-5のような最新モデルから、特定のタスクに特化したモデル、さらには無料で利用できるモデルまで、数百種類ものLLMがラインナップされています。ユーザーは自分の好みや予算に応じてモデルを自由に選択できます。
  • 💸 従量課金制: ユーザーは使った分だけクレジットを支払うシンプルな料金体系です。

つまり開発者は作成するソフトだけコスト管理すればよく、API利用料そのものはユーザーに負担して貰うため、管理負荷が劇的に下がるというわけです。

個人開発アプリへのOpenRouter導入戦略

それでは、「NovelADV」の事例を元に、OpenRouterをアプリに組み込むための3つのステップを見ていきましょう。

ステップ1: 🔐 ユーザー認証とAPIキーの安全な取得 (OAuth 2.0 PKCE)

最も重要なのは、ユーザーのAPIキーを安全に取得し、管理することです。アプリ内のテキストフィールドにAPIキーを直接入力させる方法は、ペーストボードからの漏洩リスクなどがあり、推奨されません。

最も安全でモダンな方法は、OAuth 2.0のPKCE (Proof Key for Code Exchange) フローを導入することです。これにより、ユーザーはアプリ内で安全にOpenRouterにログインし、アプリに必要な権限(APIキーの生成)を与えることができます。

「NovelADV」では、AuthenticationServicesフレームワークのASWebAuthenticationSessionを使ってこれを実現しています。

OpenRouterOAuthManager.swift
import AuthenticationServices

final class OpenRouterOAuthManager: NSObject, ASWebAuthenticationPresentationContextProviding {
    
    // ... (各種URLやプロパティの定義) ...
    private var pkce = PKCE.generate()
    private var csrfState = UUID().uuidString
    
    func startAuthentication(completion: @escaping (Result<String, Error>) -> Void) {
        // 認証フローを開始するたびに、新しいPKCEとCSRF stateを生成
        self.pkce = PKCE.generate()
        self.csrfState = UUID().uuidString

        // 認証用のURLを構築
        var urlComponents = URLComponents(string: authorizationURL)!
        urlComponents.queryItems = [
            .init(name: "callback_url", value: webCallbackURL), // アプリにリダイレクトするための中継ページ
            .init(name: "code_challenge", value: pkce.challenge),
            .init(name: "code_challenge_method", value: "S256"),
            .init(name: "state", value: csrfState) // CSRF対策
        ]
        
        // ... (authURLの生成) ...

        // ASWebAuthenticationSessionを初期化して開始
        let session = ASWebAuthenticationSession(
            url: authURL,
            callbackURLScheme: "noveladv" // Info.plistで設定したカスタムURLスキーム
        ) { [weak self] callbackURL, error in
            // コールバックURLから認可コード(code)を取得
            guard let self = self, let url = callbackURL else { /* ... エラー処理 ... */ return }
            
            // ... (stateの検証など) ...
            
            guard let code = getCodeFrom(url) else { /* ... エラー処理 ... */ return }

            // 取得した認可コードをAPIキーと交換する
            self.exchangeCodeForApiKey(code: code, completion: completion)
        }
        
        session.presentationContextProvider = self
        session.start()
    }
    
    // ... (exchangeCodeForApiKeyの実装) ...
}

このフローで取得したAPIキーは、必ずKeychainに保存します。UserDefaultsに平文で保存するのは絶対にやめましょう。

KeychainService.swift
final class KeychainService {
    static let shared = KeychainService()
    
    // APIキーをキーチェーンに保存する
    func saveApiKey(_ apiKey: String, for provider: Provider, profile: String = "default") throws {
        // ... (SecItemAdd / SecItemUpdate の実装) ...
    }

    // キーチェーンからAPIキーを取得する
    func getApiKey(for provider: Provider, profile: String = "default") -> String? {
        // ... (SecItemCopyMatching の実装) ...
    }
}

ステップ2: 🔌 統一されたAPIクライアントの実装

OpenRouterのAPIはOpenAIのフォーマットと互換性があるため、実装は非常にシンプルです。しかし、将来的にローカルLLMなど他のAIバックエンドを追加する可能性を考慮し、プロトコルで処理を抽象化しておくことを強く推奨します。

LLMProvider.swift
// LLMプロバイダーが準拠すべきプロトコル
protocol LLMProvider {
    func chat(
        messages: [(role: ChatRole, content: String)],
        config: LlmConfig,
        onStream: @escaping @MainActor @Sendable (String) -> Void
    ) async throws -> String
    
    func cancelActiveStream()
}

このプロトコルに準拠したOpenRouterProviderを実装します。

OpenRouterProvider.swift
final class OpenRouterProvider: LLMProvider {
    // ...

    func chat(
        messages: [(role: ChatRole, content: String)],
        config: LlmConfig,
        onStream: @escaping @MainActor @Sendable (String) -> Void
    ) async throws -> String {
        // 1. KeychainからAPIキーを取得
        guard let apiKey = KeychainService.shared.getApiKey(for: .openRouter) else {
            throw LLMError.apiKeyNotSet
        }

        // 2. リクエストボディを作成
        let requestBody = OpenRouterChatRequest(
            model: config.model,
            messages: messages.map { OpenRouterMessage(role: $0.role.rawValue, content: $0.content) },
            stream: config.stream
            // ... その他のパラメータ
        )

        // 3. URLRequestを作成 (ヘッダーにAPIキーとアプリ名をセット)
        var request = URLRequest(url: URL(string: "https://openrouter.ai/api/v1/chat/completions")!)
        request.httpMethod = "POST"
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("NovelADV/1.8 (iOS)", forHTTPHeaderField: "X-Title") // アプリ名を送信
        request.httpBody = try JSONEncoder().encode(requestBody)

        // 4. ストリーミング or 非ストリーミングでリクエストを実行
        if config.stream {
            return try await performStreamingRequest(request: request, onStream: onStream)
        } else {
            // ... (通常のdata(for:)メソッドでリクエスト) ...
        }
    }
    
    // ...
}

ステップ3: 🎨 モデル選択UIと価格情報の活用

OpenRouterの真価は、その豊富なモデル選択肢にあります。ユーザーが自由にモデルを選べるUIを提供しましょう。

OpenRouterの/modelsエンドポイントを叩くことで、利用可能なモデルの一覧とその価格情報を取得できます。

OpenRouterProvider.swift
// モデルの価格情報を表す構造体
struct Pricing: Codable {
    let prompt: String     // 1M入力トークンあたりの価格(ドル)
    let completion: String // 1M出力トークンあたりの価格(ドル)
}

// モデル情報を表す構造体
struct OpenRouterModel: Codable, Identifiable {
    let id: String
    let name: String
    let pricing: Pricing
}

// モデル一覧を取得するメソッド
func fetchAvailableModels() async throws -> [OpenRouterModel] {
    let url = URL(string: "https://openrouter.ai/api/v1/models")!
    // ... (APIキーをヘッダーに含めてリクエスト) ...
    let decodedResponse = try JSONDecoder().decode(OpenRouterModelsResponse.self, from: data)
    return decodedResponse.data
}

取得したモデルリストを使って、検索機能付きの選択画面を作成します。「NovelADV」では、さらに価格情報を利用して、特定の価格以上のモデルを「プレミアムモデル」として扱っています。

LLMConfigViews.swift
struct ModelSelectionListView: View {
    // ...
    @EnvironmentObject private var purchaseManager: PurchaseManager
    
    // ...
    
    // モデルが有料プラン専用かどうかを価格に基づいて判定
    private func isPremiumModel(basedOn pricing: Pricing) -> Bool {
        guard let promptPrice = Double(pricing.prompt) else { return false }
        // 例: 1M入力トークンあたり$1.00以上ならプレミアム扱い
        return promptPrice >= 1.0e-6 
    }
    
    // ...
    
    // ListのRow部分
    private struct ModelSelectionRow: View {
        // ...
        var body: some View {
            Button(action: action) {
                HStack {
                    VStack(alignment: .leading) { /* モデル名など */ }
                    Spacer()
                    // 無料ユーザーかつプレミアムモデルの場合、鍵アイコンを表示
                    if !purchaseManager.isSubscribed && isPremiumModel(basedOn: model.pricing) {
                        Image(systemName: "lock.fill").foregroundStyle(.secondary)
                    }
                    if isSelected {
                        Image(systemName: "checkmark")
                    }
                }
            }
        }
    }
}

💸 課金モデル

NovelADV」ではモデル選択UIと価格情報くを利用し、以下のような権限管理を行っています。

  • 無料ユーザー:
    • OpenRouterが提供する無料モデルのみ利用可能。
    • プレミアムモデルを選択しようとすると、課金画面 (SubscriptionView) へ誘導される。
  • プレミアムユーザー:
    • すべてのモデル(GPT-5, Grok4など)が利用可能になる。

このロジックは、モデル選択UIやAPIリクエストを送信する直前のチェックで簡単に行うことができます。

この戦略の素晴らしい点は、開発者がAPIコストを直接負担することなく、"高性能モデルへのアクセス権"という明確な付加価値をユーザーに提供できることです。ユーザーは自分のOpenRouterクレジットで好きなだけ物語を生成でき、開発者は安定したサブスクリプション収益を得ることができます。

まとめ

OpenRouterは、個人や小規模チームの開発者が直面するLLMの「コストと運用の壁」という問題を解決する、非常に強力なツールだと感じました。

開発者はコストの心配をすることなく、最新・最強のAIモデルを組み込んだ創造性あふれるアプリケーションの開発に集中できます。AIアプリ開発するときは、OpenRouterの導入を検討してみてはいかがでしょうか?

自分が創作した世界を冒険しながら小説が書けるiOSアプリ「NovelADV」は無料配信中

Discussion