カテゴリー: AI・API

  • YouTube Analytics APIの403は権限ではなくAPI有効化が原因だった

    YouTube Analytics APIの403は権限ではなくAPI有効化が原因だった

    この記事で分かること

    YouTube Analytics APIを叩いて403が返ってきたとき、真っ先に権限(スコープ)を疑って時間を溶かした話です。結論としては原因は権限ではなく、Google Cloudのそのプロジェクトで YouTube Analytics API が有効化されていなかっただけでした。同じ403で止まっている人が、エラーメッセージのどこを読めばいいかが分かります。

    起きたこと:APIを叩いたら403

    YouTube Analytics APIにリクエストを投げたところ、レスポンスが403で返ってきました。認証は通っているつもりだったので、最初は何が起きているのか分かりませんでした。

    HTTPステータスの403は「禁止」なので、反射的に「アクセス権がない」と読んでしまいます。私もそう読みました。そしてそれが間違いでした。

    最初の判断:スコープが足りないと思い込んだ

    403を見た時点で、私は「OAuthのスコープが足りていない」と判断しました。YouTube Analytics系はスコープの指定を間違えやすいという先入観があったので、そこを疑うのが自然に感じたのです。

    ただ、この判断には根拠がありませんでした。ステータスコードだけを見て、レスポンスの中身であるエラーメッセージをきちんと読んでいなかったからです。

    実際の原因:APIが有効化されていなかった

    エラーメッセージには、こう書かれていました。

    has not been used in project N

    「このプロジェクトでまだ使われていない」と書いてあります。権限が足りないとは一言も書いていません。つまりこれは、権限の話ではなく、Google Cloud のそのプロジェクトで YouTube Analytics API が有効化されていない、という意味でした。

    スコープをいじっても、認証情報を作り直しても、この403は消えません。プロジェクト側でAPI自体がオフのままだからです。

    対処:コンソールで1クリック

    Google Cloud コンソールで該当プロジェクトの YouTube Analytics API を有効化しました。作業としては1クリックです。その後に同じリクエストを投げたら、あっさり通りました。

    直す作業そのものは一瞬でした。時間がかかったのは、原因を「権限」だと思い込んでいた間の調査です。エラーメッセージを最初に読んでいれば、この回り道はありませんでした。

    学び:403を見たらメッセージ本文を読む

    今回の教訓は単純です。403 = 権限不足、と決めつけないことです。403という数字は同じでも、その裏にある理由は複数あります。今回のように、そもそもAPIが有効化されていない場合も403で返ってきます。

    • ステータスコードだけで原因を推測しない
    • レスポンスのエラーメッセージ本文を必ず読む
    • has not been used in project という文言が出ていたら、それは権限ではなく有効化の問題
    • スコープや認証情報をいじり始める前に、メッセージが何を言っているかを確認する

    まだ分かっていないこと

    YouTube Analytics APIが返す403のうち、どのパターンが権限起因で、どのパターンが有効化起因なのか、その全体像までは私は把握していません。今回確認できたのは、「has not been used in project」というメッセージが出るケースは有効化の問題だった、という一件だけです。それ以外のメッセージが出た場合に同じ対処が効くかどうかは、私の手元では検証できていません。

    また、なぜプロジェクトで有効化されていない状態のまま認証だけ通ってしまうのか、その仕組み上の理由についても、私はきちんと説明できるだけの情報を持っていません。

    まとめ

    • YouTube Analytics APIを叩いたら403が返ってきた
    • 反射的にスコープ(権限)の問題だと判断したが、それは誤りだった
    • エラーメッセージには「has not been used in project N」と書かれていた
    • 実際の原因は、Google Cloud のそのプロジェクトで YouTube Analytics API が有効化されていなかったこと
    • Cloud コンソールで1クリック有効化したら、リクエストは通った
    • 403を見たら権限不足と決めつけず、まずエラーメッセージ本文を読むこと
    • 今回確認できたのはこの1パターンのみで、他の403の原因までは検証できていない
  • YouTube Data APIのクォータ設計:探すほうが書くより高い

    YouTube Data APIのクォータ設計:探すほうが書くより高い

    この記事で分かること

    YouTube Data APIには1日10,000ユニットというクォータ上限があります。この記事では、実際にコメント投稿を自動化して運用してみて分かった「検索のほうが投稿より高い」というコスト構造と、1日5件のコメント設計が実際に何ユニット消費するのか、そしてチャンネルを増やすときにクォータをどう分けているかを書きます。

    クォータの単位は「リクエスト回数」ではない

    YouTube Data APIのクォータは1日10,000ユニットです。ここで注意が必要なのは、この「ユニット」がリクエスト回数と一致しないことです。エンドポイントごとに消費するユニット数が違います。

    search.list          100ユニット/回
    commentThreads.insert 50ユニット/回
    videos.list            1ユニット/回
    channels.list          1ユニット/回
    

    この表を初めて見たとき、私は順番が逆だと思いました。書き込み系のほうが重いはずだ、という思い込みがあったからです。実際には、動画を検索するsearch.listが100ユニットで、コメントを投稿するcommentThreads.insertの50ユニットの2倍です。

    「何に返信するか探す」ほうが「実際に書く」より高い

    これは設計に直接効いてきます。コメント自動投稿の処理は、大きく分けると次の2段階です。

    • どの動画にコメントするかを探す(search.list:100ユニット)
    • 実際にコメントを書き込む(commentThreads.insert:50ユニット)

    つまり、コストの重心は「書く」ではなく「探す」側にあります。投稿件数を減らしてもクォータはあまり減らず、検索の呼び方を変えたほうが効きます。逆に言えば、一度の検索結果を使い回して複数件投稿する設計は、クォータの観点では素直に安くなります。

    また、videos.listやchannels.listは1ユニットです。この差は100倍あります。動画IDが既に分かっているなら、search.listで探し直すのではなくvideos.listで取りに行く。これだけでコストが2桁変わるので、実装するときは「今この処理は探しているのか、既に知っているものを引いているのか」を意識するようにしています。

    1日5件コメントする設計の実際の消費量

    私が運用している構成は、1日5件コメントするというものです。計算するとこうなります。

    検索  100 × 1回 = 100ユニット
    投稿   50 × 5回 = 250ユニット
    ------------------------------
    合計            350ユニット
    (上限 10,000ユニット)
    

    350ユニットです。上限10,000に対して余裕があります。この数字を出す前は、クォータが足りるかどうかが不安要素でしたが、実際に計算してみると心配していたレベルとは桁が違いました。

    注意しておきたいのは、これはあくまで「検索1回・投稿5回」という設計の場合の数字だということです。検索の回数を増やせばここは一気に膨らみます。100ユニットの処理を1日に何十回も回す設計なら、投稿件数が同じでもクォータの話は別物になります。

    チャンネルを増やすときはプロジェクトを分ける

    クォータは Google Cloud プロジェクト単位で割り当てられます。そのため、チャンネルごとにGoogle Cloudプロジェクトを分けると、クォータも別々になります。

    私は現在2チャンネルを、それぞれ別プロジェクトで運用しています。この構成にしておくと、片方のチャンネルで検索処理を増やしても、もう片方のクォータには影響しません。1プロジェクトにまとめて10,000ユニットを2チャンネルで奪い合う形にしなかったのは、そこが理由です。

    ただし、この分け方が規模を増やしたときにどこまで通用するのかは、私はまだ確かめていません。2チャンネルで問題なく動いている、というところまでが実際に分かっていることです。

    設計するときに見ている順番

    • まず、その処理は「探す」のか「既知のものを引く」のかを分ける
    • 探す(search.list)なら100ユニットなので、1日に何回呼ぶかを先に決める
    • 投稿(commentThreads.insert)は50ユニットなので、件数×50で見積もる
    • videos.list / channels.list は1ユニットなので、ここは基本的に気にしていない

    クォータ設計というと難しそうに聞こえますが、実際にやっていることは「100の処理を1日何回呼ぶか決める」だけに近いです。1ユニットの処理をいくら呼んでも、100ユニットの検索1回のほうが重いからです。

    まとめ

    • YouTube Data APIのクォータは1日10,000ユニット。エンドポイントごとに消費量が違う
    • search.listは100ユニット、commentThreads.insertは50ユニット。探すほうが書くより高い
    • videos.list / channels.list は1ユニット。既知のIDを引くだけなら検索の100分の1で済む
    • 1日5件コメントする設計なら、検索100 + 投稿250 = 350ユニット。上限には余裕がある
    • クォータの見積もりは、投稿件数より「検索を1日何回呼ぶか」で決まる
    • チャンネルごとにGoogle Cloudプロジェクトを分ければクォータも別々になる。実際に2チャンネルを別プロジェクトで運用中
    • ただし、この分け方がチャンネル数を増やしたときにどうなるかは、まだ確かめていない
  • Claude APIを毎日運用して遭遇したエラーと対処(自動リトライが効かない罠)

    Claude APIを毎日運用して遭遇したエラーと対処(自動リトライが効かない罠)

    Claude APIで記事を毎日自動生成しています。その運用中に実際に起きた障害と、その対処を記録します。公式ドキュメントに書かれていない挙動が中心です。

    1. 「Grammar compilation timed out」でSDKのリトライが効かない

    構造化出力(JSON Schema指定)を使っていると、まれにこのエラーが返ります。

    Error code: 400 - 'Grammar compilation timed out.'

    問題はこれが400番(クライアントエラー)で返ることです。AnthropicのSDKにはmax_retriesがありますが、400番は「リクエストが悪い」という扱いなので再試行されません。実際には一時的な混雑で起きるだけなので、もう一度投げれば通ります。

    対処として、このメッセージのときだけ自前で再試行するようにしました。

    for i in range(4):
        try:
            return client.messages.create(...)
        except Exception as e:
            if "Grammar compilation timed out" not in str(e):
                raise          # 他のエラーは再試行しない
            time.sleep(20 * (i + 1))

    ポイントはこのエラーだけを対象にすることです。全部の400番を再試行すると、本当にリクエストが壊れているときに気づけなくなります。

    2. 長い出力はストリーミングにしないとタイムアウトする

    max_tokensを大きくした長文生成では、リクエスト全体のタイムアウトに引っかかることがあります。長い入力・長い出力・大きなmax_tokensのいずれかに当てはまるなら、最初からストリーミングを使うほうが安全です。

    3. コストは「生成」より「探す」ほうが高いことがある

    これはClaudeに限りませんが、外部APIと組み合わせるときに効いてきます。たとえばYouTube Data APIでは、動画を検索する操作が1回100ユニット、コメントを投稿する操作が50ユニットです。1日の上限は10,000ユニット。

    つまり「何に返信するか探す」処理のほうが、実際に書く処理より高いという状態が起きます。自動化を設計するときは、生成コストだけでなく探索コストを数えてください。

    4. エラー通知は「型」と「最後の1行」を必ず残す

    運用初期、エラーログにstr(e)だけを記録していたところ、中身が空のまま失敗する例外に遭遇して原因が分からなくなりました。

    [エラー] remove @xxx:            ← これだけ。手がかりゼロ

    いまは例外の型名トレースバックの最後の1行を必ず添えています。この2つがあるだけで、調査時間が大幅に変わります。

    まとめ

    • 400番でも一時的なエラーがある。SDK任せにせず、メッセージを見て自前で再試行する
    • 長文生成はストリーミングを既定にする
    • 生成コストより探索コストのほうが高い場合がある
    • ログには例外の型を残す。str(e)だけでは空になることがある