6月6日を境に、Gemini Interactions API のレガシースキーマ(応答を outputs 配列で返す形式)が完全に削除されました。5月中は Api-Revision ヘッダーで旧形式へ一時的に戻せましたが、現在はこのヘッダー自体が無視されます。つまり「いったん戻して様子を見る」という選択肢はすでになく、動かなくなったコードは新しい steps スキーマへ書き換えて前へ進むしかありません。
私自身は5月の終わり、iOS の壁紙アプリ4本の更新作業と並行して、App Store のレビューを毎朝要約して通知する自分用ツールをこの新スキーマへ移行しました。コード量にして百行に満たない小さなツールでしたが、それでも「ドキュメント通りに直したはずなのに最後の出力が取れない」という場面が二度ありました。個人開発の自動化ツールは「動いているから触らない」が積み重なりやすく、今回のような廃止期限で初めて壊れて気づいた方も多いのではないでしょうか。
そこで、移行作業を「壊れ方の症状」から逆引きできる形で並べ直しました。unary(一発呼び出し)、ストリーミング、構造化出力、自前の会話履歴管理の順に、書き換え前後のコードを比べていきます。
何が起きたのか — 三段階で進んだスキーマ切り替え
公式の移行ガイド(Interactions API: Breaking changes migration guide)によると、切り替えは次の三段階で進みました。
- 5月6日(オプトイン開始): 新スキーマ対応 SDK が公開され、REST では
Api-Revision: 2026-05-20ヘッダーを付けたリクエストのみ新形式になりました - 5月20日(デフォルト反転): ヘッダーなしのリクエストが新スキーマへ切り替わりました。
Api-Revision: 2026-05-06を付ければ旧形式に戻せる猶予期間です - 6月6日(完全廃止): 旧スキーマが削除され、
Api-Revisionヘッダーは無視されるようになりました
変更の柱は2つあります。1つ目は応答の outputs 配列が、種別判別子(type discriminator)付きの steps 配列に置き換わったこと。2つ目は response_mime_type が廃止され、出力形式の指定が多態的な response_format に集約されたことです。
1つ、実際にガイドを読み込んで戸惑った点を共有しておきます。本文には「Python 1.76.0 以降 / JavaScript 1.53.0 以降に上げれば自動的に新スキーマになる」とある一方、タイムライン表には「Python 2.0.0 以降 / JS 2.0.0 以降」と書かれていて、要求バージョンの記載が揺れています。私は深追いせず最新安定版まで一気に上げる判断をしました。移行後のコードが新形式しか想定しない以上、SDK 側も新形式しか返さないバージョンに揃えてしまうほうが、中途半端な版で新旧が混ざるより事故が少ないという考え方です。
なお、5月7日以降に追加された新機能は steps 形式の応答にしか現れない、という記載もガイドにあります。旧形式のまま粘るメリットは、廃止日よりずっと前に実質なくなっていました。モデルIDやAPIリビジョンのような「決め」をコードに固定して差分を検出する運用は、Gemini APIの設定ドリフトを止める — モデルID・安全設定をコード化して環境差を検出するでも書いたとおり、こういう廃止イベントのたびに効いてきます。
症状から切り分ける — 「落ちる」より「無言で空になる」が厄介
手元のコードがどう壊れているかで、直す場所が決まります。私が確認した壊れ方は3通りでした。
- unary 呼び出しが例外で落ちる: Python なら
AttributeError: 'Interaction' object has no attribute 'outputs'、生 REST なら応答 JSON にoutputsキーがなくKeyErrorになります。一番分かりやすい壊れ方です - ストリーミングが例外なしで空になる: イベント名が
content.deltaからstep.deltaに変わったため、旧イベント名でフィルタしているハンドラは「該当イベントなし」として静かに素通りします。しかも判別フィールド自体がevent_typeからtypeに変わっており、二重の理由で一致しません。バッチ処理だと「成功扱いなのに成果物が空」という形で並ぶので、発見が遅れます。私のレビュー要約ツールも検証環境でこの状態になり、空の Slack 通知が3日分並んでいるのを見てようやく気づきました - 自前の会話履歴管理が 400 を返す: 前ターンの
outputsを次のinputに詰め直す実装は、新形式の応答にoutputsが存在しないため履歴が空になるか、形式不一致でリクエストが拒否されます
例外が出るケースより、静かに空になるケースのほうが運用上は厄介です。応答が空になる症状つながりでは、スキーマ変更とは別原因のGemini 2.5/3 で本文が空なのに finish_reason が MAX_TOKENS になるときの原因と対処もあわせて確認しておくと、切り分けが早くなります。
基本の書き換え — outputs[0].text は steps の model_output へ
まず unary 呼び出しの最小修正です。旧コードはこう書いていました。
# Before: 6月5日まで動いていた旧スキーマの読み方
from google import genai
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
interaction = client.interactions.create(
model="gemini-3-flash-preview",
input="このアプリレビューを1行で要約してください: ...",
)
print(interaction.outputs[0].text) # 6月6日以降: AttributeError新スキーマでは応答が steps 配列になり、思考(thought)・ツール呼び出し・モデル出力が時系列で並びます。
# After: 新 steps スキーマの読み方
from google import genai
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
interaction = client.interactions.create(
model="gemini-3-flash-preview",
input="このアプリレビューを1行で要約してください: ...",
)
# steps には thought / function_call / model_output が混在し得る。
# 本文が欲しいだけなら model_output ステップに絞って取り出すのが安全。
final_text = ""
for step in interaction.steps:
if step.type == "model_output":
final_text = step.content[0].text
print(final_text)
# 期待される出力例:
# 星1の主因はダーク表示時の白画面。次回更新での修正要望が2件。公式サンプルは interaction.steps[-1].content[0].text と末尾参照で書かれていますが、私は type == "model_output" で絞る書き方に落ち着きました。理由は2つあります。まず、思考過程が応答に含まれる設定では先頭側に thought ステップが入るため、steps[0] 参照は本文ではないものを掴みます(私が最初に引っかかったのはここでした)。次に、function calling で status が requires_action のときは最後のステップが function_call になるため、末尾参照でも本文とは限りません。
もう1つ、地味に重要な仕様差があります。POST /interactions の応答に含まれるのは出力系ステップのみですが、GET /interactions/{id} で同じインタラクションを再取得すると、user_input ステップを含む全タイムラインが返ります。インデックス位置を前提にしたコードは、POST 直後は動くのに再取得処理では1つずれる、という分かりにくい不整合を起こします。種別で絞っておけばどちらの応答でも同じコードが使えます。
ストリーミングはイベント名の総入れ替え
ストリーミングは修正箇所がはっきりしています。新旧のイベント対応は次のとおりです。
interaction.start→interaction.createdcontent.start→step.startcontent.delta→step.deltacontent.stop→step.stopinteraction.complete→interaction.status_update(status: "completed")error→interaction.status_update(status: "error")
あわせて、イベントを判別するフィールド名が event_type から type に変わっています。書き換え後の最小形はこうなります。
# After: 新スキーマのストリーミング読み取り
for event in client.interactions.create(
model="gemini-3-flash-preview",
input="昨夜のクラッシュレポートの傾向を3行で説明してください。",
stream=True,
):
# 旧: chunk.event_type == "content.delta"
if event.type == "step.delta":
if event.delta.type == "text":
print(event.delta.text, end="", flush=True)function calling をストリーミングで使っている場合は、もう一段の変更があります。step.start イベントが関数名を運び、引数は step.delta の arguments_delta として部分的な JSON 文字列で流れてくるため、受信側で連結してからパースする必要があります。unary では完全な function_call オブジェクトが一度に届くので、この差はガイドの中でも見落としやすい段落だと感じます。
終端処理にも1つ注意があります。移行ガイドの廃止イベント一覧では interaction.complete は interaction.status_update に置き換えとされている一方、同じページの新スキーマの SSE サンプルには interaction.complete が残っています。ドキュメント内で記述が揺れている間は、終端を単一のイベント名に依存させず、step.stop と status 系イベントのどちらが来てもストリームを閉じられる寛容なハンドラにしておくのが安全だと考えています。実際に私のツールでも両対応にしてあり、どちらのイベントで閉じたかをログに残して観察を続けています。
response_mime_type は廃止 — 構造化出力は response_format に集約
構造化出力(JSON モード)を使っているコードは、リクエスト側の書き換えが必要です。
# Before: response_mime_type と素の JSON スキーマを並べる旧形式
interaction = client.interactions.create(
model="gemini-3-flash-preview",
input="このレビューを分類してください: 起動直後に落ちます。星1。",
response_mime_type="application/json",
response_format={
"type": "object",
"properties": {"sentiment": {"type": "string"}},
},
)# After: type 判別子つき response_format に mime_type と schema を内包する
interaction = client.interactions.create(
model="gemini-3-flash-preview",
input="このレビューを分類してください: 起動直後に落ちます。星1。",
response_format={
"type": "text",
"mime_type": "application/json",
"schema": {
"type": "object",
"properties": {"sentiment": {"type": "string"}},
},
},
)
print(interaction.steps[-1].content[0].text)
# 期待される出力例: {"sentiment": "negative"}ポイントは、旧 response_format に直接渡していた JSON スキーマが schema キーの中へ一段深くネストされることです。response_mime_type の行を消すだけ・キー名を変えるだけの機械的な置換では直りません。画像生成の設定も同じ統合の対象で、generation_config の中にあった image_config(アスペクト比やサイズ)は response_format の type: "image" エントリへ移動しました。テキストと画像など複数モダリティを同時に要求する場合は、response_format をオブジェクトではなく配列にします。
自前の会話履歴と、見落としやすい3つの細部
最後に、規模は小さいものの直し忘れやすかった点を3つ挙げます。
- 会話履歴の持ち回り: クライアント側で履歴を管理する stateless 構成では、これまで応答の
outputsを次のリクエストのinputに渡していました。新形式ではsteps配列をそのままinputへ渡し、新しいユーザー発話をuser_inputステップとして末尾に足します - 引用(annotations)の形式変更: テキスト中の出典情報が
type: "url_citation"の判別子とtitleフィールドを持つ形になりました。Google Search グラウンディングの引用を自前でレンダリングしている場合は、描画側のコードも直す必要があります - 検索結果フィールドの改名:
google_search_resultステップの結果はresult.rendered_contentではなくresult.search_suggestionsから読みます。グラウンディング結果を DB に保存している構成では、保存スキーマの不一致が静かに進行するので早めの修正をおすすめします
このあたりは一度に全部直そうとせず、症状が出ている経路から順に潰すほうが確実でした。なお、モデル世代自体の移行(3.1 から 3.2 など)を同時に進めている場合は、Gemini 3.2 API 実装ガイド — 正しいモデルID・3.1 からの移行と本番チェックポイントで書いた本番チェックポイントと作業を分けて、スキーマ移行とモデル移行を別コミットにしておくと切り戻しが楽になります。
まとめ — まず .outputs を grep で棚卸しする
最初の一歩としておすすめしたいのは、修正よりも先にリポジトリ全体へ grep -rn "\.outputs" --include="*.py"(JavaScript なら .outputs と content.delta の両方)をかけて、当たり所を一覧にすることです。そのうえで本稿の症状3分類 — 例外で落ちる unary、無言で空になるストリーミング、形式不一致になる履歴管理 — のどれに該当するかを1ファイルずつ確認していけば、書き換え自体は小さなツールなら30分前後で終わります。2014年から個人開発の運用ツールを少しずつ増やしてきた身として、こうした廃止対応は「気づいた日のうちに棚卸しまで終わらせる」のが結局いちばん安上がりだと感じています。同じように6月6日以降の静かな空出力に気づいた方の参考になれば幸いです。