はじめに
昨年からお届けしてきた本連載も、いよいよ最終回です。前回は、APIキーをサーバーから取得し、SecureStorageに保存する方式を実装しました。しかし、この方式にはまだ課題があります。APIキーがモバイルアプリに渡されるため、通信の傍受やアプリの解析によってキーが漏洩するリスクが残っています。
今回は、より安全な方式として、サーバー側でOpenAI APIを呼び出し、クライアントには結果だけを返す「プロキシ方式」を実装します。この方式では、APIキーはサーバー側で管理され、クライアントに漏れることはありません。また最後に、本連載では詳細まで踏み込めなかったWebアプリのレンダリングモードについて、補足としてまとめておきます。
今回の変更内容
アーキテクチャを次のように変更します。
MAUI版では、サーバーに画像生成をリクエストし、サーバーがOpenAI APIを呼び出して結果を返します。Web版は、すでにOpenAIImageServiceをサーバーサイドで実行する形になっているため、構成の変更は必要ありません。
画像生成APIエンドポイントの作成
まず、サーバー側に画像生成用のAPIエンドポイントを作成します。
リクエスト・レスポンスDTOの定義
ImageGenerator.SharedプロジェクトにModelsフォルダを作成し、APIのリクエスト・レスポンスとエラー応答を表す3つのDTOクラスを定義します。
DTO(Data Transfer Object)とは、データをやり取りするためだけのシンプルなデータクラスのことです。いずれも同じエンドポイントに関連するので、1つのファイルにまとめて作成します。
// 画像生成のリクエスト(プロンプト、品質、サイズ) public record ImageGenerationRequest(string Prompt, int Quality, int Size); // 画像生成の成功応答(生成された画像のURL) public record ImageGenerationResponse(string ImageUrl); // エラー応答(ASP.NET Core標準のProblemDetailsからdetailだけを受け取る) public record ProblemResponse(string? Detail);
これらは、Sharedプロジェクトに配置することで、サーバー側とMAUI側の両方から参照できます。
ImageGenerationRequestはリクエスト用で、前回までにUIから指定できるようにしたプロンプト、品質、サイズをセットします。ImageGenerationResponseは成功時の応答用で、画像URLのみを返します(本連載ではDALL-E 3をURL形式で利用しています)。
エラーの有無はHTTPステータスコードで判断します。エラー時の応答は、サーバーがASP.NET Core標準のProblemDetails形式で返すので、クライアント側では、そのうちのdetailプロパティだけをProblemResponseオブジェクトにセットするようにします。
なお、ここで使っているrecord型はC# 9以降の機能です。record型なら、コンストラクタと読み取り専用プロパティを持つデータ用クラスをシンプルな形で定義できます。
エンドポイントの実装とサービス登録
ImageGenerator.WebプロジェクトのProgram.csに、画像生成サービスをDIコンテナに登録し、画像生成エンドポイントを追加します。
// 画像生成サービスの登録 (1)
builder.Services.AddSingleton<IImageGenerationService, OpenAIImageService>();
// 画像生成APIエンドポイントの追加 (2)
app.MapPost("/api/generate-image", async (
ImageGenerationRequest request, IImageGenerationService imageService,
ILogger<Program> logger) =>
{
// プロンプトの検証 (3)
if (string.IsNullOrWhiteSpace(request.Prompt))
{
return Results.Problem(
detail: "プロンプトを入力してください",
statusCode: StatusCodes.Status400BadRequest);
}
try
{
// OpenAI APIを呼び出して画像を生成 (4)
var imageUrl = await imageService.GenerateImageAsync(
request.Prompt, request.Quality, request.Size);
return Results.Ok(new ImageGenerationResponse(imageUrl));
}
catch (Exception ex)
{
// 詳細はサーバーログに残し、クライアントには汎用メッセージを返す (5)
logger.LogError(ex, "画像生成中にエラーが発生しました");
return Results.Problem(
detail: "画像の生成中にエラーが発生しました",
statusCode: StatusCodes.Status500InternalServerError);
}
});
まず、IImageGenerationServiceとしてOpenAIImageServiceをDIコンテナに登録します(1)。この登録は第3回から同様です。エンドポイント内では、この登録を通じてサービスを注入して利用します。
MapPostメソッドで、POSTリクエストを受け付けるエンドポイントを定義します(2)。リクエストボディのJSONは、自動的にImageGenerationRequest型にバインドされます。
リクエストのプロンプト文が空の場合は、Results.Problemメソッドでステータスコード400(Bad Request)のエラー応答を返します(3)。Results.Problemは、ASP.NET Coreのヘルパーメソッドで、ProblemDetails形式のエラー応答を生成します。レスポンスボディとして、detail、status、titleといったプロパティを含むJSONが返されます。
正常な場合は、DIで注入されたIImageGenerationServiceを使って画像を生成します。このとき、プロンプトだけでなく、品質とサイズの指定もOpenAIImageServiceに渡します(4)。Results.Okメソッドは、ステータスコード200を返し、レスポンスボディに、ImageGenerationResponseオブジェクトをJSON形式でセットします。
OpenAI APIの呼び出し中に例外が発生した場合は、サーバー側のログには詳細を記録しつつ、クライアントには汎用的なメッセージだけを500 Internal Server Errorで返します(5)。例外メッセージをそのまま外部に表示しないことで、APIキー無効や課金エラーといった内部情報の漏洩を防ぎます。
