私が個人開発で運営しているアプリの裏側には、毎晩動く小さな自動化スクリプトがいくつもあります。App Store と Google Play に寄せられたレビューを集めて分類したり、AdMob のレポートを要約したり、その多くが gemini -p "..." というシェル呼び出しに支えられていました。ところが、その前提が 6/18 で崩れます。
Google AI Pro / Ultra と Gemini Code Assist 向けの Gemini CLI は、6/18 を境にホストが応答を返さなくなり、後継の Antigravity CLI へ一本化されます。バックエンドのエージェントハーネスは同じなので、対話的に使う分には乗り換えるだけで済みます。けれども、cron から無人で叩いている自動化スクリプトは事情が違います。応答が返らなくなった瞬間、毎晩のジョブが静かに失敗し続けるからです。
この記事は、その「静かな失敗」を防ぐために私が実際にたどった移行の手順です。結論から言うと、私は CLI を別の CLI に置き換えるのではなく、自動化の部分だけ google-genai SDK へ寄せる選択をしました。その理由も含めて、移行前後で変わったことを残しておきます。
まず「CLI に何を任せていたか」を棚卸しする
移行で最初につまずくのは、コードの書き換えではありません。どのスクリプトが CLI に依存しているのかを把握しきれていない、という点です。対話的に手で叩く用途と、無人で回している用途が頭の中で混ざっていると、移行の優先順位を付けられません。
私はまず、CLI を呼んでいる箇所を機械的に洗い出しました。
# crontab と スクリプト群から gemini 呼び出しを抽出する
grep -rn -E '(^|[^a-z])gemini( |$)' ~/scripts ~/cron 2> /dev/null
crontab -l | grep -n gemini
洗い出した結果を、無人で動くかどうかで二つに分けます。手で叩く調査用途は Antigravity CLI へそのまま移してかまいません。優先して書き換えるべきは、cron やフックから人の目を介さずに動いているものだけです。私の場合は 6 本のスクリプトのうち、緊急で対応が必要な無人ジョブは 3 本でした。残りは慌てて触らない、と決めるだけでも気持ちがずいぶん楽になります。
シェル呼び出しを SDK 呼び出しに置き換える
移行前の実装は、シェルから CLI を呼び、標準出力を受け取るだけの素朴なものでした。
# 移行前: CLI にプロンプトを渡し、出力をそのまま受け取る
RESULT = $( gemini -p "次のレビュー本文を分類してください: ${ REVIEW_TEXT }" )
echo " $RESULT " >> labels.txt
これを google-genai SDK の呼び出しに置き換えます。Python を使っているなら、まず SDK を導入します。
pip install google-genai
そのうえで、CLI に渡していたプロンプトをそのまま generate_content に移します。
import os
from google import genai
# 環境変数からキーを読む。スクリプトに直書きしない
client = genai.Client( api_key = os.environ[ "GEMINI_API_KEY" ])
def classify (review_text: str ) -> str :
response = client.models.generate_content(
model = "gemini-2.5-flash" ,
contents = f "次のレビュー本文を分類してください: \n{ review_text } " ,
)
return response.text
この時点で動作は CLI 時代とほぼ同じです。ただ、ここで止めてしまうと「壊れやすさ」も一緒に引き継いでしまいます。CLI 出力をテキストとして受け取り、後段で正規表現でこじ開けていた構造を、移行のついでに作り直すのが得策です。SDK 固有のエラーや初期化でつまずいた場合は、google-genai SDK への移行で遭遇するエラーと対処 に詰まりどころをまとめています。
構造化出力で「壊れない移行」にする
CLI 時代にいちばん困っていたのは、出力が自然文だったり JSON だったりと安定しないことでした。後段のスクリプトは出力をパースして使うので、形が揺れるたびに失敗していました。SDK へ移すなら、ここで response_schema を使って出力の形を固定します。
from pydantic import BaseModel
class ReviewLabel ( BaseModel ):
sentiment: str
category: str
needs_reply: bool
def classify_structured (review_text: str ) -> ReviewLabel:
response = client.models.generate_content(
model = "gemini-2.5-flash" ,
contents = review_text,
config = {
"response_mime_type" : "application/json" ,
"response_schema" : ReviewLabel,
},
)
# parsed には ReviewLabel のインスタンスが入る
return response.parsed
スキーマを渡すと、戻り値が型付きのオブジェクトとして受け取れます。後段のコードはテキストをパースする必要がなくなり、label.needs_reply のように属性で扱えます。スキーマを無視されるなど想定外の挙動に出くわした場合は、構造化出力のスキーマ検証で失敗するときの対処 が参考になります。
リトライとレート制限への備え
CLI 時代は失敗したら翌日のジョブで拾えばいい、という雑な運用で済ませていました。けれども無人ジョブを API に直結させると、一過性の 429(レート超過)や 503(過負荷)で夜間バッチが丸ごと落ちる事故が起きやすくなります。指数バックオフで握りつぶせる失敗は握りつぶす設計にしておきます。
import time
from google.genai import errors
def classify_with_retry (review_text: str , max_attempts: int = 5 ) -> ReviewLabel:
delay = 1.0
for attempt in range ( 1 , max_attempts + 1 ):
try :
return classify_structured(review_text)
except errors.APIError as e:
# 429 / 503 は一過性なので待って再試行する
if e.code in ( 429 , 503 ) and attempt < max_attempts:
time.sleep(delay)
delay = min (delay * 2 , 30.0 )
continue
raise
raise RuntimeError ( "max_attempts exhausted" )
ここで注意したいのは、待ち時間の上限を必ず設けることです。上限なしの指数バックオフにすると、過負荷が続いたときにジョブが何時間も眠り続けます。私は最大 30 秒で頭打ちにして、それでも駄目なら諦めて翌日に回す、という割り切りにしています。レート制限そのものの考え方はGemini API のレート制限エラーを解消する に整理しました。
コストとレイテンシを移行前後で実測する
移行の判断で欠かせないのが、実際にいくらかかるかという感覚です。CLI 時代は内部でどのモデルが使われ、トークンをどれだけ消費しているのかが見えにくく、コスト感がどんぶり勘定でした。SDK なら usage_metadata から呼び出しごとのトークン数を取得できます。
def classify_and_measure (review_text: str ):
response = client.models.generate_content(
model = "gemini-2.5-flash" ,
contents = review_text,
config = {
"response_mime_type" : "application/json" ,
"response_schema" : ReviewLabel,
},
)
usage = response.usage_metadata
return {
"label" : response.parsed,
"input_tokens" : usage.prompt_token_count,
"output_tokens" : usage.candidates_token_count,
}
私の手元で、レビュー約 200 件を分類する夜間ジョブを移行前後で比べた目安が以下です。数字はプロンプト長や負荷状況で動くので、あくまで自分の環境での傾向として見てください。
観点 CLI 経由(移行前) API 直接(移行後)
1 件あたりの応答時間 約 2.4 秒 約 1.3 秒
200 件の総処理時間 約 9 分 約 5 分
出力パース失敗率 約 6% ほぼ 0%
コストの可視性 不透明 呼び出しごとに把握可能
体感でいちばん効いたのは、応答時間そのものより出力パース失敗率の改善でした。CLI 出力を正規表現でこじ開けていた頃は、20 件に 1 件ほど形が崩れて翌朝に手作業で拾っていました。構造化出力に切り替えてからは、その後始末がほぼなくなり、処理時間は約 4 割短くなりました。usage_metadata を使ったコスト追跡の本番設計はusage_metadata でコストを追跡する本番設計 に踏み込んで書いています。
CLI を捨てて API に寄せるという判断
公式の案内は Antigravity CLI への移行です。対話的な用途なら、私もそれが素直だと思います。ですが無人の自動化に関しては、私は CLI 依存そのものをやめて API に寄せる方を選びました。
理由は三つあります。第一に、CLI は人が対話するために設計された層であり、その出力フォーマットや挙動はバージョンで変わり得ます。無人ジョブの土台にするには、契約が安定している API の方が安心できます。第二に、構造化出力・リトライ・コスト計測といった本番運用に必要な道具立てが、API なら素直に手に入ります。第三に、CLI を別の CLI へ移しても「いつかまた終了の案内が来る」リスクは残ります。今回のような移行を二度繰り返したくない、というのが個人開発を続けてきた私の本音です。
逆に、調査や試行錯誤のように手で対話する用途まで API へ書き換えるのは過剰だと感じています。そこは Antigravity CLI に任せ、無人で回る部分だけ API に固める。この線引きが、限られた時間で運用する個人開発には現実的だと考えています。
移行当日までにやること
残り時間は多くありません。今日のうちに進めておきたいのは、次の一つです。まず grep で CLI 依存の無人ジョブを洗い出し、そのうち最も止まると困る一本だけを、この記事のリトライ付き・構造化出力版に置き換えてみてください。一本動けば、残りは同じ型の繰り返しになります。
私自身まだ移行の途中で、細かい調整は続けていますが、夜間ジョブが静かに止まる前に手を打てたのは大きな安心でした。同じように CLI 依存の自動化を抱えている方の備えになればうれしいです。