はじめに
シリーズ3回目です。①でセットアップ、②で全機能を体験してきましたが、順調に見えて裏では結構つまずいていました。今回はその全部を「症状 → 原因 → 解決」のFAQ形式で公開します。
| 各回のタイトル | 内容 |
|---|---|
| ①セットアップ編 | 無償プラン契約〜アカウントトークン(APIキー)発行〜最初の1リクエスト |
| ②全機能ためす編 | チャット生成/ベクトル埋め込み/音声の文字起こし/音声の読み上げ |
| ③つまずき解決編(本記事) | 実際に遭遇したエラーと解決法FAQ |
| ④まとめ編 | 30分で作るミニアプリ+無償枠の賢い使い方(予定) |
エラーメッセージで検索してたどり着いた方は、下の早見表から該当箇所へ飛んでください。
症状の早見表
| # | 症状 | 発生場面 |
|---|---|---|
| Q1 | SSL certificate OpenSSL verify result: unable to get local issuer certificate |
Windows環境 + PHPでcurlのAPI呼び出し |
| Q2 | php.iniを編集したのに反映されない/エラーが何も表示されない | Windows環境構築 |
| Q3 |
Invalid date.timezone value 'JST' という起動時Warning |
PHPインストール時のタイムゾーン設定 |
| Q4 | HTTP 400 — This model is not available. | チャットAPIの呼び出し |
| Q5 | curlコマンドでは動くのに、PHPからだと401になる | コマンドプロンプト利用者のみ |
| Q6 | ストリーミングが無言で正常終了する(エラーなし・出力なし) | 思考型モデルのストリーミング |
前半(Q1〜Q3)は環境構築系、後半(Q4〜Q6)はAPI呼び出し系です。
Q1. SSL certificate OpenSSL verify result: unable to get local issuer certificate
症状
Windows環境でPHPのcurlからAPIを呼び出すと、下記エラーが発生し通信に失敗する。
> php chat_basic.php
通信エラー: SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
原因
Windows版PHPは「信頼できる証明書のリスト(CA証明書)」の参照先が未設定のため。
LinuxではOSが持つCA証明書ストアをPHPが自動で参照しますが、Windows版PHPは何も設定されていません。そのためHTTPS接続先(さくらのAPIサーバ)の証明書を検証できず、安全のため接続を拒否します。さくら側の障害ではなく、手元のPHPの設定不足です。
解決
- curl公式のcacert.pem をダウンロード
- php.iniと同じフォルダ(場所は
php --iniで確認)に置く - php.iniに1行追加:
curl.cainfo = "C:\Users\<ユーザー名>\AppData\Local\Programs\PHP\current\cacert.pem"
やってはいけない解決法
検索すると次のコードがよく出てきますが、使わないでください。
// ダメな例: 証明書の検証を放棄する
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
エラーは消えますが、「通信相手が本物か確認しない」設定です。偽サーバに誘導されてもエラーにならず、APIキーごと送信してしまいます。エラーを消すことと問題を解決することは別物、という典型例です。
Q2. php.iniを編集したのに反映されない / エラーが何も表示されない
症状
- php.iniの
extension=curlを有効にしたはずなのにCall to undefined function curl_init()になる
PS C:\sakura-ai-engine> php chat_basic.php
PHP Fatal error: Uncaught Error: Call to undefined function curl_init() in C:\sakura-ai-engine\chat_basic.php:16
Stack trace:
#0 {main}
thrown in C:\sakura-ai-engine\chat_basic.php on line 16
Fatal error: Uncaught Error: Call to undefined function curl_init() in C:\sakura-ai-engine\chat_basic.php:16
Stack trace:
#0 {main}
thrown in C:\sakura-ai-engine\chat_basic.php on line 16
- コードにミスがあるはずなのに、実行しても何も表示されず終わる
原因
原因として、次の2パターンが考えられるので、それぞれチェックして対応が必要となります。
原因A: 編集したphp.iniが、実際に読み込まれているphp.iniと違う。 PHPが複数入っている環境や、インストーラが作る深い階層のパスでは、まったく別のファイルを編集していることがあります。
原因B: エラーは起きているが、エラー表示がOffになっている。 ②全機能ためす編で使ったPHP公式インストールスクリプトが作るphp.iniは本番環境向けの設定(php.ini-production)がベースのため、display_errors = Offになっています。PHPコードのエラーが発生していても何も表示されません。
解決
まず「本当に読み込まれているphp.ini」を特定します。
php --ini
PS C:\> php --ini
Configuration File (php.ini) Path:
Loaded Configuration File: C:\Users\hoge\AppData\Local\Programs\PHP\current\php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed: (none)
ここで表示されたphp.iniファイルを編集し、エラーを表示するよう次のように設定します。
display_errors = On
なぜ本番環境はOffなのか: エラーメッセージにはファイルパスやコードの断片が含まれ、攻撃者へのヒントになるからです。「本番はOff、手元はOn」という使い分け自体がPHP運用の基本です。
Q3. PHP Warning: Invalid date.timezone value 'JST'
症状
PHPを実行するたび、下記のようなPHP Warningが表示される。
PHP Warning: PHP Startup: Invalid date.timezone value 'JST', using 'UTC' instead in Unknown on line 0
原因
②全機能ためす編で使ったPHP公式インストールスクリプトの選択肢にて、日本時間のつもりで JST と入力し設定したため。PHPのタイムゾーン設定はタイムゾーンデータベースの名前(地域/都市の形式)しか受け付けず、JST のような略称は無効となる。無効な値は無視されてUTC扱いになるため、時刻が日本時間と9時間ずれてしまいます。
解決
php.iniで正式名を設定します。
date.timezone = "Asia/Tokyo"
②全機能ためす編のインストールスクリプトの対話形式でタイムゾーンを聞かれた際、Asia/Tokyo と入力します。ちなみにタイムゾーン略称がダメな理由は、同じ略称が世界で重複するから(例: CSTだと、アメリカ中部標準時・中国標準時など)。「都市名まで書けば一意になる」という設計になっています。
Q4. HTTP 400 — This model is not available.
症状
PHPを実行すると、下記のようなAPIエラーが表示される。
PS C:\sakura-ai-engine> php chat_basic.php
APIエラー (HTTP 400): {"error":{"message":"This model is not available."}}
原因
モデル名の指定ミス。モデル名は gpt-oss-120b のような短いものから llm-jp-3.1-8x13b-instruct4 のような長いものまであり、大文字小文字・ハイフンの位置まで完全一致が必要です。手入力するのではなく、正式名をコピーして使うのが確実です。
また、以前使えていたモデルが「提供終了」になることもあるため、定期的にチェックするようにしましょう。
解決
使えるモデルの正式名は、API自身に聞くのが確実です。
curl -s https://api.ai.sakura.ad.jp/v1/models \
-H "Authorization: Bearer <アカウントトークン>"
この一覧からコピペしてください。①セットアップ編で「最初の1リクエストはモデル一覧の取得がおすすめ」と書いたのは、これが理由です。
Q5. curlコマンドでは動くのに、PHPからだと401になる(コマンドプロンプトの怪)
個人的に今回一番面白かったつまずきです。同じ環境変数を使っているのに、curlコマンドは成功し、PHPは認証エラーになるという現象です。
症状
①セットアップ編の手順で、コマンドプロンプトの環境変数にアカウントトークンを設定します。
set SAKURA_API_KEY="<アカウントトークン>"
この状態で、curlコマンドは動きます。
curl -s https://api.ai.sakura.ad.jp/v1/models -H "Authorization: Bearer %SAKURA_API_KEY%"
→ モデル一覧が正常に返る
ところが、PHPから同じ環境変数を読むと401になります。
chat_test.php:
<?php
$apiKey = getenv('SAKURA_API_KEY');
$payload = [
'model' => 'gpt-oss-120b',
'messages' => [
['role' => 'system', 'content' => 'あなたは簡潔に答えるアシスタントです。'],
['role' => 'user', 'content' => 'HTTPとHTTPSの違いを2文で教えてください。'],
],
'max_tokens' => 300,
];
$ch = curl_init('https://api.ai.sakura.ad.jp/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$response = curl_exec($ch);
if ($response === false) {
exit('通信エラー: ' . curl_error($ch) . "\n");
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = json_decode($response, true);
if ($status !== 200) {
exit("APIエラー (HTTP {$status}): " . $response . "\n");
}
echo $data['choices'][0]['message']['content'] . "\n";
実行結果:
c:\sakura-ai-engine>php chat_test.php
APIエラー (HTTP 401): {"error":{"message":"Invalid token"}}
原因
原因は set で使った引用符です。コマンドプロンプトの set は、bashやPowerShellと違って引用符も値の一部として保存します。確認してみましょう。
echo [%SAKURA_API_KEY%]
実行結果:
c:\sakura-ai-engine>echo [%SAKURA_API_KEY%]
["3 ... F"] ← 引用符ごと入っている!(実際はアカウントトークン全文が表示されます。ここでは伏せています)
では、なぜcurlは動いたのか? 展開後のコマンドラインは次の形になります。
-H "Authorization: Bearer "<アカウントトークン>""
これをcurl.exeが引数として解釈するとき、「"Authorization: Bearer "(引用区間その1)+<トークン>+""(空の引用区間その2)」と連結処理され、引用符が偶然すべて相殺されて、正しいヘッダが送信されていたのです。curl -v を付けると、送信ヘッダに引用符が無いことが確認できます。
一方PHPの getenv() は環境変数の値をそのまま(引用符ごと)受け取るため、Bearer "<トークン>" という壊れたヘッダが送信されて401になります。
解決
コマンドプロンプトでは引用符なしで設定するのが正解です。
set SAKURA_API_KEY=<アカウントトークン>
ただ、②全機能ためす編でAPIキーを環境変数から設定ファイル(config.php)方式に切り替えたのは、まさにこの種のOS依存の罠から逃れるためでした。ファイル読み込みなら全環境で同じ挙動になります。
Q6. chatストリーミングが無言で正常終了する(エラーなし・出力なし)
②全機能ためす編で遭遇した、この連載最大のつまずきです。
症状
PS C:\sakura-ai-engine> php chat_stream.php
PS C:\sakura-ai-engine>
エラーなし。出力なし。終了コードも正常。手がかりなし。
原因
2段構えの罠でした。
その1: 思考型モデルは、本文の前に「思考」を流してくる。 gpt-oss-120bは回答前に思考(reasoning)を行うモデルで、ストリーミングでは思考トークンが delta.content ではなく別フィールド(delta.reasoning)で流れてきます。content だけを拾う単純な実装では、思考中ずっと無言になります。
その2: 思考もmax_tokensを消費する。 max_tokens: 300 程度だと、思考だけで枠を使い切り、finish_reason: length で終了。本文は1文字も生成されません。「思考の分までトークンを確保する」必要があります。
実は①セットアップ編にヒントがありました。最初のcurl実行で返ってきたJSONに、見慣れない reasoning フィールドがあったのです。あのときは「そういうものか」とスルーしましたが、ここでつまづきの原因として理解できました。
解決
3点セットで対策します。②全機能ためす編に完成版コードを掲載済みです。
-
思考の進行を可視化する:
delta.reasoningを検知したらドット等で表示 - max_tokensを多めに確保する: 思考型モデルなら2000程度
-
本文ゼロで終わったら理由を表示する:
finish_reasonをチェックし、lengthなら「トークン切れ」と表示
対策後の動作:
※ドットの数だけ思考チャンクが流れています(この例で1,000個超)。20字の質問に対する思考量として眺めてみてください
PS C:\sakura-ai-engine> php chat_stream.php
(思考中..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................)
生成AIの歴史は、1990年代後半の統計的言語モデルに始まる。2006年に深層学習が注目され、2014年のGAN登場で画像生成が革命的に変化。2018年のGPTが自然言語生成を大幅に向上させ、以降、ChatGPTやStable Diffusionなど多様なモデルが実用化され、日常や産業に浸透している。教育や医療でも応用が広がり、倫理議論が活発化している。今後は制御と創造性の調和が重要になる時代だ。
コラム: モデルgpt-oss-120bが「私はChatGPTです」と名乗る件
①セットアップ編の実行結果をよく見ると、gpt-oss-120bが自己紹介で「ChatGPT」と名乗っていました。これはエラーではありません。gpt-ossはOpenAIが公開したオープンウェイトモデルで、学習の由来から自分をChatGPTと認識することがあります。AIの自己申告は正確とは限らないので、モデルの正体はAPIレスポンスの model フィールドで確認するのが確実です。
まとめ
6つのつまずきを並べると、共通する教訓が3つ見えてきます。
- エラーを消すことと、問題を解決することは別物(Q1の検証オフ、Q5の「たまたま動く」)
- 「何も表示されない」には必ず理由がある(Q2のdisplay_errors、Q6の思考トークン)
-
困ったら、思い込みではなく事実を確認するコマンドを叩く(
php --ini、echo [%VAR%]、curl -v、/v1/models)
3つ目は生成AIに限らず、サーバ運用のトラブルシューティングと完全に同じ対応です。生成AIのAPIといっても、結局は「HTTPSで利用できる外部サービス」。普段の障害調査のやり方がそのまま通用できます。
なお、この記事のために再現実験で消費したリクエストは9回。累計でもまだ枠の1%程度です。

次回予告: ④まとめ編
最終回は、ここまでの学びを全部使って30分で作れるミニアプリを1本仕上げます。あわせて、無償枠(チャット3,000/埋め込み10,000/音声各50)の賢い配分についても連載のまとめとして書く予定です。
