直叩きから api.aicu.ai へ
実際に社内で動いている Slack Bot(Aicuty Bot)と記事生成ツールを移行したときの記録です。
なぜ書き換えるのか
移行前、画像生成のコードはこうなっていました。
- Slack Bot は
OPENAI_API_KEYを持ち、api.openai.comを直叩き - 記事生成ツールは
GEMINI_API_KEYを持ち、generativelanguage.googleapis.comを直叩き - どちらも、失敗したときのエラーは実行環境の標準出力か Slack のメッセージにしか残らない
この構成の実務上の困りごとは、鍵が増えることより 「何がどれだけ使われたか分からない」 ことでした。実際、移行前に本番ログを集計したら、LLM 1,325 リクエスト分の利用が台帳に 1 件も記録されていませんでした。請求の根拠が無い状態です。
移行の原則: 呼び出し口を1つにする
api.aicu.ai は OpenAI 互換なので、エンドポイントとキーを差し替えるだけで動きます。
| 移行前 | 移行後 | |
|---|---|---|
| エンドポイント | プロバイダごとに別 | https://api.aicu.ai/v1 |
| キー | OPENAI_API_KEY + GEMINI_API_KEY + … | AICU_API_KEY 1本 |
| モデル指定 | SDK ごとの書き方 | model 引数 |
| 利用の記録 | 各自でやる | ダッシュボードに自動で残る |
手順1: クライアントを1枚かませる
いきなり全部書き換えず、薄いモジュールを1枚作って差し込むのが安全でした。環境変数で切り替わるので、問題があれば元の経路に即戻せます。
# aicu_images.py
import base64, os, requests
def is_enabled() -> bool:
return bool(os.environ.get("AICU_API_KEY"))
def generate(prompt, *, model="gpt-image-2", character=None,
reference_image=None, mask=None, size=None, quality=None,
aspect_ratio=None, timeout=240):
payload = {"model": model, "prompt": prompt}
if character:
payload["character"] = character
if reference_image:
payload["reference_image"] = base64.b64encode(reference_image).decode()
if mask:
payload["mask"] = base64.b64encode(mask).decode()
for k, v in (("size", size), ("quality", quality), ("aspect_ratio", aspect_ratio)):
if v:
payload[k] = v
res = requests.post(
"https://api.aicu.ai/v1/images/generations",
headers={"Authorization": f"Bearer {os.environ['AICU_API_KEY']}"},
json=payload, timeout=timeout,
)
res.raise_for_status()
data = res.json()
return base64.b64decode(data["data"][0]["b64_json"]), bool(data.get("cached"))
手順2: 呼び出し側を差し替える(フォールバック付き)
+ if aicu_images.is_enabled():
+ try:
+ image_data, cached = aicu_images.generate(
+ prompt, model="gpt-image-2", character=member,
+ reference_image=ref_bytes, mask=mask_bytes,
+ size="1536x1024", quality="high",
+ )
+ except aicu_images.AicuImageError as exc:
+ print(f"[gpt-image2] AICU gateway failed: {exc}")
+
+ if image_data is None:
oai = OpenAI(api_key=OPENAI_API_KEY)
response = oai.images.edit(model="gpt-image-2", image=..., prompt=prompt)
image_data = base64.b64decode(response.data[0].b64_json)
AICU_API_KEY を設定するまで挙動は 1 ミリも変わりません。設定して初めてゲートウェイ経由になります。この「入れても何も起きない状態」を先に本番へ出しておくと、切り替えが怖くなくなります。
手順3: Gemini からの移行
Gemini の SDK は形がだいぶ違いますが、置き換えるとむしろ短くなります。
- client = genai.Client(api_key=GEMINI_API_KEY)
- response = client.models.generate_content(
- model="gemini-3.1-flash-image-preview",
- contents=prompt,
- config=types.GenerateContentConfig(
- response_modalities=["IMAGE", "TEXT"],
- image_config=types.ImageConfig(aspect_ratio="16:9"),
- ),
- )
- for part in response.candidates[0].content.parts:
- if part.inline_data:
- output_path.write_bytes(part.inline_data.data)
+ data, cached = aicu_images.generate(
+ prompt, model="nano-banana", aspect_ratio="16:9"
+ )
+ output_path.write_bytes(data)
モデル名は移行元のものをそのまま渡しても通ります(gemini-3.1-flash-image-preview → nano-banana にエイリアス解決)。まず動かして、あとから短い名前に直せます。
つまずいた点
参照画像をどう渡すか。 最初は R2 に事前アップロードする設計でしたが、移行元のツールは手元に参照画像を持っています。そこで reference_image に base64 を直接渡せるようにしました。事前登録が不要になり、移行が「1関数の差し替え」で済むようになりました。
マスク編集を落とさない。 Slack Bot にはマスク画像を添付して部分編集する機能がありました。ゲートウェイ側に mask を通したことで、機能を削らずに移行できています。移行で機能が減ると、現場は元に戻します。
キーの scope。 発行済みのキーは ["tts"] しか持っておらず、画像を呼ぶと 403 でした。α版のキーは llm / tts / images を最初から持つように変更しました。
移行して得られたもの
- 失敗が見える。 どのモデルが何回失敗したかがダッシュボードに残ります。移行前に気づけなかった「OpenRouter のクレジット切れで 707 回失敗していた」ようなことが即分かります。
- キャッシュが効く。 同じ条件の再生成は R2 から返り、課金されません(
cached: true)。 - 原価が分かる。 モデル別の粗利と損益分岐が出せるので、単価の妥当性を数字で議論できます。
- キャラクター使用料を按分できる。 → キャラクター活用API
Support
- Dashboard: https://api.aicu.ai/dashboard
- 利用履歴: https://api.aicu.ai/dashboard/history
© 2026 AICU Inc.