はじめに
LLM API gateway を本番寄りに触っていると、たまに provider_connection_error や upstream connection failed 系のエラーに当たります。
私が最初にハマったのは、これを見た瞬間に「自分の API key か model 指定が悪いのでは」と寄せすぎたことでした。実際には、TCP 接続の失敗、provider 側の一時障害、router 側の設定、model route の指定ミスが同じような見た目になることがあります。
この記事では local fixture で connection error を再現し、request_id、route、model、provider_status、retry_count を 1 セットで残す形にしました。実在 provider の障害を断定する記事ではありません。
3行まとめ
-
provider_statusがnullなら、provider から HTTP response を受け取る前に落ちた可能性を見る。 -
provider_statusが401、404、429、5xxなら、接続ではなく status ごとの切り分けに移る。 -
request_id、route、model、provider_status、retry_countを同じログ行に置くと、upstream 起因と設定ミスを混ぜにくいです。
再現環境
今回の fixture は Python 標準ライブラリだけで作りました。
python3 agents/qiita-growth-agent/results/qiita_issue_321_provider_connection_fixture.py
やっていることは単純です。
- 使われていない localhost port に POST して connection refused を起こす。
- gateway が返す想定の
provider_connection_errorbody とログ行を作る。 - 同じ route/model で mock upstream を立て直し、
200が返る fixed path を確認する。
実 provider には接続していないので、これは outage の証明ではなく、ログ設計の再現 fixture です。
エラー全文
local fixture で残したエラー body はこれです。
{
"error": {
"message": "provider_connection_error: upstream connection failed before an HTTP response was received",
"type": "provider_connection_error",
"code": "upstream_connection_failed",
"param": null
},
"request_id": "req_321_bad_upstream"
}
この時点では provider_status がありません。HTTP response を受け取れていないので、502 や 503 と同じ表で雑に扱うと少し危ないと思います。
ログで見る順番
私が最初に見るログは、エラー文そのものよりこの 1 行です。
{"client_request_id":"client_321","duration_ms":24,"error_code":"upstream_connection_failed","error_type":"provider_connection_error","event":"llm_gateway.upstream_connection_failed","level":"error","model":"gpt-5.4-nano","provider":"local-fixture-upstream","provider_status":null,"request_id":"req_321_bad_upstream","retry_count":2,"route":"chat.completions.primary","timestamp":"2026-07-17T00:30:00Z","upstream_host":"127.0.0.1"}
見る順番はだいたい次のようにしています。
| 見る項目 | 見方 |
|---|---|
request_id |
ユーザー報告、gateway log、provider log をつなぐ軸にする |
route |
どの routing rule に入ったかを見る |
model |
model 名の typo、非対応 model、別 provider 向け model ではないかを見る |
provider_status |
null なら HTTP 前、数字なら status table に移る |
retry_count |
1 回だけの失敗か、retry 後も同じ失敗かを見る |
ここで provider_status: null、retry_count: 2 なら、私はまず DNS、network、TLS、provider endpoint 到達性、proxy 設定を見ます。逆に provider_status: 401 なら API key や provider account、404 なら endpoint/model route、429 なら quota と rate limit に寄せます。
upstream 寄りに見るケース
provider_status が null のときは、まだ provider から HTTP として怒られていない状態です。ここでいきなり model 名を直したり key を差し替えたりすると、たまたま直ったように見えて原因が残ることがあります。
私はこの順番で見ます。
- 同じ provider の別 model も失敗しているか。
- 同じ route の fallback 先も失敗しているか。
- 特定の runtime、region、container だけで失敗していないか。
- proxy や egress firewall の変更が直近にないか。
- provider の status page や社内 incident channel に同時刻の情報がないか。
特に Connection refused、名前解決失敗、TLS handshake 失敗、timeout は、アプリの payload より前で落ちています。もちろん gateway の upstream URL 設定ミスでも同じ症状になるので、ここでは provider 障害と断定せず、「HTTP response 前の失敗」とだけ置くのがよさそうです。
設定ミス寄りに見るケース
一方で、見た目が connection 系でも provider_status に数字が入っているなら、私は普通の HTTP 切り分けに戻します。
401 なら key、header、provider account の権限を見ます。404 なら endpoint path、model 名、OpenAI 互換 endpoint と provider 固有 endpoint の取り違えを見ます。429 なら rate limit、quota、billing、同時実行数を見ます。5xx は upstream 側の一時失敗かもしれませんが、自分の request が大きすぎる、streaming 設定が provider と合っていない、特定 route だけ古い upstream URL を向いている、ということもあります。
このへんを全部エラー名だけで見るとつらいので、route と model を同じ行に残しておくのが効きました。provider_connection_error と書いてあっても、どの route に入り、どの provider に行こうとして、どこまで HTTP として進んだかを分けるだけで、調査の初手がかなり変わります。
解決した状態
mock upstream を立てた fixed path では、同じ route/model で 200 が返りました。
{"client_request_id":"client_321","duration_ms":0,"error_code":null,"error_type":null,"event":"llm_gateway.upstream_request_succeeded","level":"info","model":"gpt-5.4-nano","provider":"local-fixture-upstream","provider_status":200,"request_id":"req_321_fixed_path","retry_count":0,"route":"chat.completions.primary","timestamp":"2026-07-17T00:30:00Z","upstream_host":"127.0.0.1"}
この fixture では、設定の考え方としては「同じ route と model のまま、upstream endpoint が到達可能になれば成功する」を確認できました。実環境ならここに provider の status page、region、proxy、egress firewall、router の upstream URL、model route の fallback 設定を重ねて見ます。
実装するときの最小ログ
本文用に少し短くすると、私はこのくらいを allowlist にします。
SAFE_LLM_LOG_FIELDS = {
"request_id",
"client_request_id",
"route",
"model",
"provider",
"provider_status",
"retry_count",
"duration_ms",
"error_type",
"error_code",
}
def llm_error_log(event: dict) -> dict:
return {key: event.get(key) for key in SAFE_LLM_LOG_FIELDS}
messages、prompt、request_body、response_body、authorization、api_key は入れません。調査したい気持ちはあるのですが、あとから消すのがつらいログを増やすより、まず operational field をそろえる方が現実的だと思います。
Flatkey AI ではどこを見るか
Flatkey AI のような OpenAI 互換 gateway を挟む場合、私はアプリ側のログと gateway 側の visibility を分けて見ます。
アプリ側では request_id と client_request_id を出し、どの user action から来た呼び出しかを追えるようにします。gateway 側では route、model、usage、provider status、retry の見え方を確認します。Flatkey AI は複数 provider account の routing と load balancing、key、usage、billing を dashboard で見る前提なので、connection error のときも「自分の app config」と「upstream 到達性」を分ける確認材料になります。
ただし、dashboard に出ている失敗だけで provider 障害と断定しない方がよいです。同じ時間帯に同じ provider だけ失敗しているのか、特定 route だけなのか、特定 model だけなのかを request_id 単位で見るのが大事だと思います。
まとめ
provider_connection_error を見たとき、私はまず自分の設定ミスを疑いがちでした。今は次の順番にしています。
-
request_idで対象 request を固定する。 -
routeとmodelで routing/model 指定を確認する。 -
provider_statusがnullか数字かを見る。 -
retry_countで一時失敗か継続失敗かを見る。 -
nullなら DNS/network/TLS/proxy/upstream endpoint、数字なら HTTP status ごとの切り分けに移る。
エラー名だけだと upstream 由来か設定ミスかを混ぜやすいので、ログの 1 セットを先に決めておくのが一番効いた気がします。
間違いあったらコメントください。よろしくお願いします。