個人で動かしている分類処理を無料枠から有料枠へ移した日、切り替えの作業そのものは10分で終わりました。長かったのはそのあとです。「これで送ったものがモデルの学習に使われることはない」と自分に言い切ってよいのか、確認のために規約を頭から読み直しました。
読み終えて分かったのは、その理解が半分だけ正しいということでした。有料枠かどうかで扱いが変わるのは事実です。ただ、有料枠の側にも、扱いが無料枠と同じところへ戻る操作が用意されています。私はそれを知らないまま、ログ機能を有効にしようとしていました。
以下は、その日に自分用のメモとして整理した内容を、公式の文言に当て直したものです。押さえる点は3つでした。
- 有料扱いの判定基準が、AI Studio と Gemini API で違います。 同じ Google アカウントでも、画面での試行は有料扱い、API 呼び出しは無料扱い、という食い違いが起こり得ます。
- 有料枠でも、ログをデータセットにして Google と共有した分は、無料枠と同じ条項で処理されます。 共有はオプトインなので、触らなければ起きません。
- 住んでいる地域と、アプリを届ける地域によって、1と2の前提が変わります。
順に見ていきます。なお、以下は規約の読み方の整理であって、法的な助言ではありません。契約上の判断が必要な場面では、原文と専門家の確認を挟んでください。
どこからが「有料扱い」になるのか
ここが最初のつまずきどころでした。Gemini API の追加利用規約は、AI Studio と API で有料サービスの定義を別々に書いています。
| 対象 | 有料サービスとして扱われる条件 |
|---|---|
| Google AI Studio | 使っているアカウントが、有効な Cloud 請求先アカウントに紐づく Cloud プロジェクトにアクセスできる場合。無料で使っていても有料扱いになります(Workspace の企業アカウントも同様です) |
| Gemini API | 有効な請求先アカウントに紐づく Cloud プロジェクト経由でアクセスしたときのみ |
読み比べると、AI Studio 側の条件のほうが緩いことが分かります。請求先が有効なプロジェクトへ「アクセスできる」ことが条件なので、その画面で無料のまま試していても有料サービスの扱いです。
一方 API のほうは、呼び出しに使ったキーがどのプロジェクトのものかで決まります。個人開発では、試行錯誤用に別プロジェクトを立てたまま忘れている、ということが起こりがちです。私も検証用のキーを1つ残していました。
この2つの違いが、そのまま送ったデータの扱いの違いになります。
| 無料(Unpaid Services) | 有料(Paid Services) | |
|---|---|---|
| プロンプトと応答の利用 | Google の製品・サービス・機械学習技術の提供と改善に使われます | 製品の改善には使われません |
| 人によるレビュー | 品質向上のため、レビュアーが読み、注釈を付け、処理する場合があります | — |
| ログの保存 | — | 禁止用途ポリシー違反の検知と、法令・規制上の開示のために限られた期間だけ保存されます |
| 公式の注意書き | 機微・機密・個人情報を送らないこと | — |
無料枠の側に「レビュアーが読む場合がある」と明記されている点は、読み飛ばしやすいところだと思います。レビュー前に Google アカウント・API キー・Cloud プロジェクトから切り離す、という説明も同時に書かれていますが、切り離されるのは紐づけであって、本文ではありません。App Store や Google Play へ出す前の説明文、アプリ内の実データ、問い合わせの本文。こうしたものを無料枠へ流していないか、ここで一度手を止める価値があります。
有料枠だけが持てるログと、その保持期間
課金を有効にしたプロジェクトでは、リクエストからレスポンスまでを自分のものとして残すログ機能が使えます。これは既定で有効になるものではなく、プロジェクトのオーナーが選んで有効にするものです。
保持期間の既定は最大55日で、期間を過ぎたログは自動的に削除対象になります。AI Studio 側の設定で、7日・14日・28日・55日から選び直せます。
ここまでは、自分のプロジェクトの中に閉じた話です。ログは非公開で、禁止用途の監視だけを目的に保持される別系統のログとも切り離されています。運用の実際については、AI Studio のログ設定を切ったときに何が一緒に止まるかを扱った記事も合わせてご覧いただくと、設定同士のつながりが見えやすいと思います。
データセットにした分だけ、扱いが戻ります
私が読み落としかけていたのは、ここでした。
ログの中から関心のあるものを選んでデータセットを作ると、そのデータセットには保持期間が設定されません。55日の時計の外に出ます。これ自体は、評価用のデータを手元に残すための機能なので、素直な仕様です。
問題は次です。作ったデータセットを Google と共有すると、その中のリクエストとレスポンスは Unpaid Services(無料サービス)の条項に従って処理されます。つまり、Google の製品・サービス・機械学習技術の開発と改善に使われる可能性があり、モデルの改善と学習も含まれます。人によるレビューの対象にもなります。
有料枠へ移したことで得た「製品改善には使われない」という状態が、この操作の分だけ元に戻る、という構造です。公式ドキュメントも、有料サービスを通じて得た機微・機密・専有の情報を含むログは提供しないでください、と太字で書いています。
大事なのは、これがオプトインだということです。何もしなければ起きません。逆に言えば、「有料枠にしたから安心」という一文で理解を止めていると、共有ボタンを押す手が止まらなくなります。私自身が危なかったのは、まさにその状態でした。評価用のデータを溜めたいという動機と、共有してもよいかという判断は、本来まったく別の話です。
地域によって前提が変わります
もう一段、条件が重なります。
欧州経済領域・スイス・英国にいる場合、無料で提供されるサービスにも有料サービスのデータ利用条項が適用されます。無料枠であっても、プロンプトと応答が製品改善に使われない側に入るということです。
さらに、これらの地域の利用者に向けてアプリを提供する場合は、有料サービスのみを使うことと明記されています。個人開発でも、アプリを世界公開にしていれば無関係ではありません。
加えて、Google 検索や Google マップによるグラウンディングを使う場合は、プロンプト・こちらが渡した文脈情報・出力が30日間保存され、グラウンディングを支える仕組みのデバッグとテストに使われます。有料枠であっても保存自体は発生します。料金の観点からの整理は、Gemini API の料金体系と無料枠の考え方をまとめた記事のほうが詳しいです。
送る前に、手元で決めておくこと
困ったのは、いま自分の呼び出しが無料枠と有料枠のどちらで動いているのか、レスポンスを見ても分からないことです。トークン数もモデル名も返ってきますが、扱いの区分は返ってきません。
分からないものを推測させるとバグになります。そこで私は、推測をやめて、呼び出し側に宣言させる形にしました。
from dataclasses import dataclass
SENSITIVITY_ORDER = {"public": 0, "internal": 1, "confidential": 2}
class SendBlocked(Exception):
pass
@dataclass(frozen=True)
class CallPolicy:
tier: str # "unpaid" / "paid" — 推測せず、呼び出し側が宣言する
share_datasets: bool # ログをデータセットにして共有する設定か
def max_allowed(self) -> str:
if self.tier == "unpaid":
return "public"
if self.share_datasets:
return "public"
return "confidential"
def guard(text: str, sensitivity: str, policy: CallPolicy) -> str:
if sensitivity not in SENSITIVITY_ORDER:
raise ValueError(f"unknown sensitivity: {sensitivity}")
limit = policy.max_allowed()
if SENSITIVITY_ORDER[sensitivity] > SENSITIVITY_ORDER[limit]:
raise SendBlocked(
f"tier={policy.tier} share_datasets={policy.share_datasets} "
f"では {sensitivity} を送れません(上限 {limit})"
)
return textshare_datasets を判定に入れているのが、この記事の要点をコードにした部分です。有料枠であっても共有を有効にしているなら、送ってよい上限は無料枠と同じところまで下がります。
4通りを流すと、こうなります。
BLOCKED confidential tier=unpaid share=False -> tier=unpaid share_datasets=False では confidential を送れません(上限 public)
OK confidential tier=paid share=False
BLOCKED confidential tier=paid share=True -> tier=paid share_datasets=True では confidential を送れません(上限 public)
OK public tier=unpaid share=False2行目と3行目の差が、共有設定ひとつで動くところです。設定画面の側では小さなトグルに見えますが、送ってよいものの範囲はこれだけ変わります。
共有を検討する段になったら、先にフォルダ単位で数えておくと判断が早くなります。
import json
import pathlib
from collections import Counter
RECORDS = pathlib.Path("records")
def tally(directory: pathlib.Path) -> Counter:
counts = Counter()
for path in sorted(directory.glob("*.json")):
record = json.loads(path.read_text())
counts[record.get("sensitivity", "unlabeled")] += 1
return counts
counts = tally(RECORDS)
total = sum(counts.values())
for key in ("public", "internal", "confidential", "unlabeled"):
if counts[key]:
print(f"{key:<13} {counts[key]:>3} / {total}")
blocking = counts["internal"] + counts["confidential"] + counts["unlabeled"]
print("共有可否:", "このまま共有できます" if blocking == 0 else f"共有前に {blocking} 件の仕分けが必要です")手元の20件で試した出力です。
public 12 / 20
internal 5 / 20
confidential 3 / 20
---
共有可否: 共有前に 8 件の仕分けが必要ですunlabeled を差し止め側に数えているのは、意図してのことです。ラベルが付いていないものは「安全」ではなく「まだ判断していない」だからです。
正直に書いておくと、この仕組みが保証できるのは「宣言と実態が一致していること」までです。tier="paid" と書いたキーが実は別プロジェクトのものだった場合、コードは何も気づけません。宣言の正しさは、キーとプロジェクトの対応を人が一度確かめるしかありません。
今日やるなら、ひとつだけ
いま使っている API キーが、どの Cloud プロジェクトのものかを確認してください。請求先が紐づいているプロジェクトかどうかは、そこで決まります。私の手元では、確認して初めて、検証用のキーが無料枠のまま残っていることに気づきました。
長くなりましたが、規約の読み直しは面倒な作業です。同じ時間を使わずに済むところがあれば嬉しく思います。お読みいただきありがとうございました。