さくらのAI Engineをコーディング支援AIとして活用する(on PhpStorm/Intellij)

AI・高火力 # コーディング支援 # さくらのAI Engine

ネットアシスト開発チームのyu-kinjoです。今回は、さくらのクラウドの推論AI API マネージドサービス「さくらのAI Engine」を、昨今広く活用されるようになったコーディング支援AIエージェントの推論モデルとして活用する方法をご紹介します。

今回はJetBrains社製のIDE(統合開発環境)である、PhpStormでさくらのAI EngineのAPIを活用してみたいと思います。同じJetBrains製IDEであるIntellij、PyCharmなどもほぼ同じ手順でご利用いただけるかと思います。

また、大まかな手順はOpenAI互換APIやAnthropic互換APIを連携させられる他のコーディング支援ツールでも同様にご活用いただけるかと思います。

「さくらのAI Engine」についてのおさらい

「さくらのAI Engine」は、さくらインターネット社のさくらのクラウドが提供する、推論AIモデルをAPIとして提供するマネージドサービスです。比較的安価な料金で「gpt-oss-120b」や「Kimi-K2.6」などのモデルをOpen AI互換APIやAnthropic互換APIとして利用する事が出来ます。

APIの利用内容はモデルの学習には使用されず、またクラウド提供企業、データセンターの立地などが日本国内で完結し、国内の法律が適用される点もポイントです。

さくらのAI Engineでマネージドな「gpt-oss-120b」をAPI利用する

さくらのAI Engineの料金プランを比較する

さくらのAI Engine | さくらのAI | 生成AI・GPUクラウドサービス

さくらのAI Engine側の設定

まずはさくらのAI Engine側で利用の為のアカウントトークンを作成します。

さくらのクラウドのアカウント登録、さくらのAI Engine画面から利用プランの選択など、一番最初に必要になる設定を済ませましたら、「アカウントトークン」画面からトークンを作成します。

作成後に表示されるアカウントトークンは再度表示する手段が有りませんので、適切にお手元に控えておいてください。

このアカウントトークンがAPIに接続する際の鍵情報になります。

PhpStorm側の設定

現在のPhpStorm(バージョン 2026.2.0.1)は、ベータ扱いとはなりますが標準で外部のAIプロバイダーに対応しています。以下のように設定を進めます。

「設定」→「ツール」→「AI Assistant」→「プロバイダー & API キー」

■サードパーティーAIプロバイダー
プロバイダー: OpenAI 互換
URL: https://api.ai.sakura.ad.jp/v1
APIキー: 先程作成したアカウントトークンの内容を入力

■AI補完
プロバイダー: OpenAI 互換
ベースURL: https://api.ai.sakura.ad.jp/v1
APIキー: 先程作成したアカウントトークンの内容を入力

「接続のテスト」ボタン→「接続完了」表示を確認。

接続テストを行った後は「コア機能」からモデルが選択できるので、今回は OpenAiApi/preview/Kimi K-2.6 を選んでみました。

これで本来の設定は完了です(続きの対応があり、後述します)。

では、適当な新規PHPファイルを作成し、コンテキストメニューの「AIアクション」からコードの生成を試してみます。

今回は Y-m-d 形式で日付を表示する処理をAIに記述してもらいます。生成したい内容を入力しEnterキーで決定します。

……が、エラーになります。これはPhpStormの固有の問題が有るからです。続きを説明していきます。

PhpStomでOpenAI互換APIがエラーになる理由

エラー内容の詳細はメニューの「ヘルプ」→「エクスプローラーでログを表示」でログの場所を表示し、idea.log ファイルを開く事で確認できます。そこでは以下のようなエラーが出力されていました。

該当箇所の抜粋:

Status code: 400
Error body:
{"error":{"message":"1 validation error:\n  {'type': 'value_error', 'loc': ('body',), 'msg': 'Value error, `tools` must not be an empty array. Either provide at least one tool or omit the field entirely.', 'input': {'messages': [{'content': 'Current date...

この内容なのですが、どうやら内容が空配列の tools を渡すことがエラーの原因となっているようです。内容が空であれば tools を省略すれば動作する物と思われますが、PhpStormは商用ツールですのでこちらで修正する事は難しいです。

では、どうしましょうか。OpenAIのAPIは単純なHTTP(S)のリクエストですので、通信内容からtools配列を取り除くプロキシーを追加すれば解決できます。

tools配列を取り除く中継サーバーをローカルで起動する

という訳でPHPのファイル1個だけで動作する、tools配列を取り除くためのサーバーを実装しました。Windows 11 + WSL2 (Ubuntu)上に導入したPHPで、PHPビルトインサーバーを動作せる事で動作します。

想定PHPバージョンは8.3以降です。PHPには curl 拡張が必要です。

WSL2上で動作させているのは、PHPビルトインサーバーのマルチスレッド動作がWindowsネイティブ上ではサポートされない事が理由になっています。Windowsネイティブ上のPHPで動作させた場合、PhpStormから連続でリクエストが送信された場合に正常に動作しない可能性が有ります。

コードの内容は以下です。

<?php
/**
 * さくらのAI Engine 向けローカルプロキシ
 *
 * JetBrains AI Assistant が送信する `"tools": []`(空配列)を除去して
 * upstream の vLLM に中継する。SSE ストリーミングをそのまま透過する。
 *
 * 起動:
 *   PHP_CLI_SERVER_WORKERS=4 php -S 127.0.0.1:8080 sakura-proxy.php
 *
 * IDE 側の Base URL:
 *   http://127.0.0.1:8080/v1
 */

declare(strict_types=1);

const UPSTREAM = 'https://api.ai.sakura.ad.jp';

/** モデル名の "openai/" などのプレフィックスを除去するか */
const STRIP_MODEL_PREFIX = true;

/** 標準エラー出力にリクエストの概要を出すか(php -S のコンソールに出る) */
const DEBUG = true;

set_time_limit(0);
ini_set('output_buffering', '0');
ini_set('zlib.output_compression', '0');
ini_set('implicit_flush', '1');
ob_implicit_flush(true);
while (ob_get_level() > 0) {
    ob_end_flush();
}

/**
 * 標準エラー出力にログを書く。
 */
function proxy_log(string $message): void
{
    if (!DEBUG) {
        return;
    }
    $fp = fopen('php://stderr', 'w');
    if ($fp !== false) {
        fwrite($fp, $message);
        fclose($fp);
    }
}

/**
 * リクエストヘッダから Authorization を取り出す。
 */
function incomingAuthHeader(): ?string
{
    if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
        return $_SERVER['HTTP_AUTHORIZATION'];
    }
    if (!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
        return $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
    }
    if (function_exists('getallheaders')) {
        foreach (getallheaders() as $name => $value) {
            if (strcasecmp($name, 'Authorization') === 0) {
                return $value;
            }
        }
    }
    return null;
}

/**
 * リクエストボディを補正する。
 *
 * - 空の tools 配列を削除(削除時は tool_choice / parallel_tool_calls も落とす)
 * - モデル名のベンダープレフィックスを除去
 */
function normalizeBody(array $body): array
{
    if (array_key_exists('tools', $body)) {
        $tools = $body['tools'];
        if ($tools === null || (is_array($tools) && count($tools) === 0)) {
            unset($body['tools'], $body['tool_choice'], $body['parallel_tool_calls']);
            proxy_log("[proxy] removed empty tools\n");
        }
    }

    if (STRIP_MODEL_PREFIX && isset($body['model']) && is_string($body['model'])) {
        $original = $body['model'];
        // "preview/" はさくら側の正規のプレフィックスなので保持する
        if (!str_starts_with($original, 'preview/') && str_contains($original, '/')) {
            $body['model'] = substr($original, strrpos($original, '/') + 1);
            proxy_log("[proxy] model: {$original} -> {$body['model']}\n");
        }
    }

    return $body;
}

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$uri    = $_SERVER['REQUEST_URI'] ?? '/';
$url    = UPSTREAM . $uri;

proxy_log("[proxy] {$method} {$uri}\n");

$auth = incomingAuthHeader();
if ($auth === null) {
    http_response_code(401);
    header('Content-Type: application/json');
    echo json_encode(['error' => ['message' => 'Authorization header is missing', 'type' => 'proxy_error']]);
    exit;
}

$headers = [
    'Authorization: ' . $auth,
    'Accept: ' . ($_SERVER['HTTP_ACCEPT'] ?? 'application/json'),
];

$payload = null;

if ($method === 'POST' || $method === 'PUT' || $method === 'PATCH') {
    $raw         = file_get_contents('php://input') ?: '';
    $contentType = $_SERVER['CONTENT_TYPE'] ?? 'application/json';

    if (stripos($contentType, 'application/json') !== false) {
        $decoded = json_decode($raw, true);
        if (is_array($decoded)) {
            $payload = json_encode(normalizeBody($decoded), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        } else {
            // JSON として解釈できない場合はそのまま流す
            $payload = $raw;
        }
        $headers[] = 'Content-Type: application/json';
    } else {
        // multipart(音声の文字起こしなど)はそのまま透過
        $payload   = $raw;
        $headers[] = 'Content-Type: ' . $contentType;
    }
}

$ch = curl_init($url);

$headersSent = false;

curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => $method,
    CURLOPT_HTTPHEADER     => $headers,
    CURLOPT_RETURNTRANSFER => false,
    CURLOPT_CONNECTTIMEOUT => 15,
    CURLOPT_TIMEOUT        => 0,     // ストリーミングのため無制限
    CURLOPT_BUFFERSIZE     => 512,
    CURLOPT_HEADERFUNCTION => function ($ch, string $line) use (&$headersSent): int {
        $len     = strlen($line);
        $trimmed = trim($line);

        if ($trimmed === '') {
            return $len;
        }

        // ステータス行
        if (preg_match('#^HTTP/[\d.]+\s+(\d{3})#', $trimmed, $m) === 1) {
            http_response_code((int) $m[1]);
            $headersSent = true;
            return $len;
        }

        // 中継するヘッダのみホワイトリストで転送
        [$name, $value] = array_pad(explode(':', $trimmed, 2), 2, '');
        $name  = trim($name);
        $value = trim($value);

        if (in_array(strtolower($name), ['content-type', 'cache-control', 'x-request-id'], true)) {
            header("{$name}: {$value}");
        }

        return $len;
    },
    CURLOPT_WRITEFUNCTION  => function ($ch, string $chunk): int {
        echo $chunk;
        flush();
        return strlen($chunk);
    },
]);

if ($payload !== null) {
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
}

curl_exec($ch);

if (curl_errno($ch) !== 0) {
    $message = curl_error($ch);
    proxy_log("[proxy] curl error: {$message}\n");
    if (!$headersSent) {
        http_response_code(502);
        header('Content-Type: application/json');
        echo json_encode(['error' => ['message' => $message, 'type' => 'proxy_error']]);
    }
}

curl_close($ch);

これを、sakura-proxy.php として保存します。やっている事としては、渡ってきたJSONからtools配列を削除する事、渡ってきた認証情報を取り出す事、その内容を元に新しいCurlリクエストを作成して結果を返す、をしています。

では、これをWSL2上で実行します。

$ PHP_CLI_SERVER_WORKERS=4
$ php -S 127.0.0.1:8080 sakura-proxy.php

次に、ローカル上を参照するように先程のPhpStorm設定画面で以下のように設定を変更します。

URL/ベースURL: http://127.0.0.1:8080/v1

この状態で再度接続テストを実行し、接続が出来たら中継の成功です。さくらのAI Engine上のモデルが選択~利用できる状態になりました。

改めてAIコーディング支援を試してみる

改めてコードの生成を試してみます。

コードが生成されました! また、生成されたコードの内容も問題ありません。

「すべて承認」を押すと変更が反映されます。

まとめ

さくらのAI Engineを用いて、IDE上で実際にコーディング支援が利用できることが確認出来ました。

PhpStormではJetBrains社自身が提供するAIサブスクリプションサービスも有るのですが、さくらのAI Engineに切り替え、または併用する事でよりコストを安価に利用する等の活用可能性が考えられます。また、PhpStorm側については、今回のように中継サーバーを立てることなくそのまま動くようになる事にも期待したいですね。

この記事を書いた人

yu-kinjo

ネットアシスト開発部の yu-kinjo です。

【取得資格】

・さくらのクラウド検定

・AWS Certified Solutions Architect - Associate

・AWS Certified AI Practitioner

・Oracle 認定Javaプログラマ SE6

・JSTQB テスト技術者資格 ファンデーションレベル

・CIW (Certified Internet Webprofessional) ファンデーション

・XML技術者育成推進委員会 XMLマスター ベーシックV2

・基本情報処理技術者

・初級システムアドミニストレータ