2026年7月17日金曜日

Gemini CLI + litellm で独自のエンドポイントを使う方法

Gemini CLI + litellm で独自のエンドポイントを使う方法

概要

前回 Codex 版を紹介しましたが今回は Gemini CLI での方法を紹介します

環境

  • Ubuntu 24.04
  • nodejs 22.21.0
  • gemini-cli 0.51.0

インストール

  • npm install -g @google/gemini-cli

構成概要

  • gemini-cli -> python proxy (docker) -> litellm (docker) -> custom endpoint

という流れになっています
python proxy は nginx などでも代用できるケースはありますが今回はリクエストを削る必要があるため python (fastapi) を使っています

理由は custom endpoint が gemini に完全に互換しているわけではなく使える機能により受け付けないパラメータがあるためそれを削るために用意しています
なので custom endpoint が完全互換であれば litellm だけで OK です

compose.yaml

gemini-proxy が python 製のプロキシになります
fastapi で gemini-cli から来たリクエストを受け使えないパラメータを削った後 litellm にリクエストします

services:
  litellm:
    image: docker.litellm.ai/berriai/litellm:latest
    container_name: litellm
    environment:
      - AI_SERVICE_API_KEY=${AI_SERVICE_API_KEY}
    ports:
      - "4000:4000"
    volumes:
      - ./litellm_config.yaml:/app/config.yaml
    restart: unless-stopped
    command: ["--config", "/app/config.yaml"]

  gemini-proxy:
    # Python proxy for gemini CLI (native Gemini API format).
    # Replaces nginx because request body JSON transformation is required:
    # strips unsupported 'id' field from function_call/function_response,
    # rewrites :streamGenerateContent?alt=sse → :streamGenerateContentSse,
    # and injects AI_SERVICE_API_KEY as x-goog-api-key header.
    # Configure gemini CLI to use port 4001 instead of 4000.
    build:
      context: .
      dockerfile: Dockerfile.proxy
    container_name: gemini-proxy
    environment:
      - AI_SERVICE_API_KEY=${AI_SERVICE_API_KEY}
    ports:
      - "4001:4001"
    restart: unless-stopped

proxy.py

API を互換させるためのプロキシです
先ほども紹介しましたが custom endpoint が完全互換の場合は不要です

#!/usr/bin/env python3
"""
Proxy for gemini CLI → Your custom Gemini endpoint.

Handles:
- URL mapping: :streamGenerateContent?alt=sse → :streamGenerateContentSse
- Request body: strip unsupported 'id' field from function_call / function_response
- Auth: replace incoming key with AI_SERVICE_API_KEY as x-goog-api-key header
"""

import json
import os

import httpx
from fastapi import FastAPI, Request
from fastapi.responses import Response, StreamingResponse

CUSTOM_BASE = os.environ.get(
    "CUSTOM_BASE",
    "https://your-gemini-custom-endpoint",
)
API_KEY = os.environ["AI_SERVICE_API_KEY"]

app = FastAPI()


def strip_unsupported_fields(body: dict) -> dict:
    """Strip 'id' from function_call/function_response — field not supported by this endpoint."""
    for content in body.get("contents", []):
        for part in content.get("parts", []):
            for key in ("functionCall", "function_call"):
                if isinstance(part.get(key), dict):
                    part[key].pop("id", None)
            for key in ("functionResponse", "function_response"):
                if isinstance(part.get(key), dict):
                    part[key].pop("id", None)
    return body


@app.post("/v1beta/models/{model}:{method}")
async def proxy(request: Request, model: str, method: str):
    is_streaming = method == "streamGenerateContent"
    # Custom endpoint uses a dedicated SSE endpoint instead of ?alt=sse query param
    target_method = "streamGenerateContentSse" if is_streaming else method
    target_url = f"{CUSTOM_BASE}/{model}:{target_method}"

    body_bytes = await request.body()
    try:
        body = strip_unsupported_fields(json.loads(body_bytes))
        body_bytes = json.dumps(body, ensure_ascii=False).encode()
    except (json.JSONDecodeError, AttributeError):
        pass

    upstream_headers = {"Content-Type": "application/json", "x-goog-api-key": API_KEY}

    if is_streaming:

        async def generate():
            async with httpx.AsyncClient(timeout=120.0) as client:
                async with client.stream(
                    "POST", target_url, content=body_bytes, headers=upstream_headers
                ) as resp:
                    async for chunk in resp.aiter_bytes():
                        yield chunk

        return StreamingResponse(
            generate(),
            media_type="text/event-stream",
            headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
        )
    else:
        async with httpx.AsyncClient(timeout=120.0) as client:
            resp = await client.post(
                target_url, content=body_bytes, headers=upstream_headers
            )
        return Response(
            content=resp.content,
            status_code=resp.status_code,
            media_type=resp.headers.get("content-type", "application/json"),
        )

Dockerfile.proxy

先程の proxy.py をビルドしてイメージを作成する dockerfile です

FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi httpx uvicorn
COPY proxy.py .
CMD ["uvicorn", "proxy:app", "--host", "0.0.0.0", "--port", "4001"]

litellm_config.yaml

gemini-3.1-flash-lite 側は基本不要ですが gemini-cli がデフォルトで gemini-3.1-flash-lite のモデルの定義を探しに行くのでエラーにならないように追加しています
pass_through_endpoints でパスのオーバライドをしていますがこれも状況によるので必須ではないです

model_list:
  - model_name: gemini-3.5-flash
    litellm_params:
      # gemini/ prefix is required to use API key auth instead of Vertex AI ADC.
      # Model name must match model_name above to avoid LiteLLM response model override error
      # that corrupts streaming responses. drop_params: true handles unsupported fields.
      model: gemini/gemini-3.5-flash
      api_base: https://your-gemini-custom-endpoint:generateContent
      api_key: os.environ/AI_SERVICE_API_KEY
      max_tokens: 4096
      temperature: 0.2
      rpm: 30
      tpm: 60000
  - model_name: gemini-3.1-flash-lite
    litellm_params:
      # gemini/ prefix is required to use API key auth instead of Vertex AI ADC.
      model: gemini/gemini-3.5-flash
      api_base: https://your-gemini-custom-endpoint:generateContent
      api_key: os.environ/AI_SERVICE_API_KEY

litellm_settings:
  drop_params: true

general_settings:
  global_max_parallel_requests: 1
  max_parallel_requests: 1
  # Pass-through: forward native Gemini API requests (/v1beta/models/...) directly
  # to the upstream endpoint, bypassing LiteLLM's model routing and SDK layer.
  # This avoids the "cannot override response model" bug caused by GenerateContentResponse
  # not having a 'model' attribute in LiteLLM's google_genai provider path.
  pass_through_endpoints:
    - path: "/v1beta/models/{path:path}"
      target: "https://your-gemini-custom-endpoint/gemini/{path}"
      headers:
        x-goog-api-key: os.environ/AI_SERVICE_API_KEY

router_settings:
  num_retries: 0
  retry_after: 20
  allowed_fails: 1
  cooldown_time: 180
  timeout: 120
  stream_timeout: 120

server_settings:
  port: 4000

動作確認

  • docker compose up -d
  • export GOOGLE_GEMINI_BASE_URL="http://localhost:4001"
  • gemini --model=gemini-3.5-flash

で「hi」とか打ってレスポンスが返ってくればとりあえず接続はできています

最後に

gemini-cli + litellm + custom endpoint の設定方法を紹介しました

独自のエンドポイントが提供している gemini などがあり API が完全互換していない場合は自分でリクエストやレスポンスを細工する必要があります
今回は fastapi + litellm で対応しましたが方法は様々なので自分に合う方法で互換させれば OK です

基本はエラーに合わせてプロキシや litellm 側で調整するしかないです

{
  "error": {
    "code": 400,
    "message": "Invalid JSON payload received. Unknown name \"id\" at 'contents[2].parts[0].function_call': Cannot find field.\nInvalid JSON payload received. Unknown name \"id\" at 'contents[3].parts[0].function_response': Cannot find field.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "field": "contents[2].parts[0].function_call",
            "description": "Invalid JSON payload received. Unknown name \"id\" at 'contents[2].parts[0].function_call': Cannot find field."
          },
          {
            "field": "contents[3].parts[0].function_response",
            "description": "Invalid JSON payload received. Unknown name \"id\" at 'contents[3].parts[0].function_response': Cannot find field."
          }
        ]
      }
    ]
  }
}

参考サイト

2026年7月8日水曜日

Ubuntu glab にコマンドをインストールするスクリプト

Ubuntu glab にコマンドをインストールするスクリプト

概要

シェルスクリプトを紹介します

環境

  • Ubuntu 24.04
  • glab 1.106.0

スクリプト

  • vim install_glab_on_ubuntu.sh
set -euo pipefail

sudo apt-get update
sudo apt-get install -y curl tar jq

ARCH_RAW="$(uname -m)"
case "$ARCH_RAW" in
  x86_64) ARCH_SUFFIX="amd64" ;;
  aarch64|arm64) ARCH_SUFFIX="arm64" ;;
  *)
    echo "Unsupported architecture: $ARCH_RAW" >&2
    exit 1
    ;;
esac

API_URL="https://gitlab.com/api/v4/projects/gitlab-org%2Fcli/releases/permalink/latest"
RELEASE_JSON="$(curl -fsSL "$API_URL")"
TAG_NAME="$(echo "$RELEASE_JSON" | jq -r '.tag_name')"

# ここが重要: 現在の命名は glab_<ver>_linux_<arch>.tar.gz
DOWNLOAD_URL="$(echo "$RELEASE_JSON" | jq -r --arg arch "$ARCH_SUFFIX" '
  .assets.links[]
  | select((.name // "") | test("_linux_" + $arch + "\\.tar\\.gz$"))
  | (.direct_asset_url // .url)
' | head -n1)"

if [[ -z "$DOWNLOAD_URL" || "$DOWNLOAD_URL" == "null" ]]; then
  echo "No matching asset URL found for arch: $ARCH_SUFFIX" >&2
  echo "DEBUG: available asset names:" >&2
  echo "$RELEASE_JSON" | jq -r '.assets.links[].name' >&2
  exit 1
fi

TMP_DIR="/tmp/glab-install"
TARBALL_PATH="$TMP_DIR/glab.tar.gz"

echo "DEBUG: ARCH_RAW=$ARCH_RAW"
echo "DEBUG: ARCH_SUFFIX=$ARCH_SUFFIX"
echo "DEBUG: API_URL=$API_URL"
echo "DEBUG: TAG_NAME=$TAG_NAME"
echo "DEBUG: DOWNLOAD_URL=$DOWNLOAD_URL"
echo "DEBUG: TMP_DIR=$TMP_DIR"
echo "DEBUG: TARBALL_PATH=$TARBALL_PATH"

rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR"

curl -fL "$DOWNLOAD_URL" -o "$TARBALL_PATH"
tar -xzf "$TARBALL_PATH" -C "$TMP_DIR"

GLAB_BIN="$(find "$TMP_DIR" -type f -name glab | head -n1)"
if [[ -z "$GLAB_BIN" ]]; then
  echo "glab binary not found in extracted archive" >&2
  exit 1
fi

sudo install -m 0755 "$GLAB_BIN" /usr/local/bin/glab
glab --version

解説

いろいろやっていますが https://gitlab.com/gitlab-org/cli/-/releases ここから tar をダウンロードし PATH 上に配置しているだけです

最後に

あとは glab auth でログインしましょう

export GITLAB_TOKEN='glpat-c-xxx'
glab auth login --hostname gitlab.devops.nifcloud.net --token <<< "$GITLAB_TOKEN"
glab auth status --hostname gitlab.devops.nifcloud.net

2026年7月7日火曜日

code-server でファイルを開く際に必ず新しいタブで開くようにする方法

code-server でファイルを開く際に必ず新しいタブで開くようにする方法

概要

タイトルの通り

環境

  • code-server 4.126.0

対策方法

"workbench.editor.enablePreview": false

2026年7月6日月曜日

Mac で Ctrl+g で勝手に Gemini が起動するのを止める方法

Mac で Ctrl+g で勝手に Gemini が起動するのを止める方法

概要

Chrome のショートカットキーで勝手に割り当てられるようになってしまったようです

環境

  • macOS 26.5.1
  • Chrome 149.0.7827.201

方法

  1. chrome://settings/ai/gemini を開く
  2. キーボードショートカットで Gemini を開きますのバツボタンを押す

これが

こうなれば OK です

最後に

たぶんそのうちデフォルトで無効になるはずです

参考サイト

2026年7月4日土曜日

codex を Ubuntu で動かす場合のサンドボックス設定

codex を Ubuntu で動かす場合のサンドボックス設定

概要

自動化する場合には書き込めるように codex がサンドボックスを作成できるようにしてあげる必要があります

環境

  • Ubuntu 24.04
  • codex 0.142.5

主なエラー

Unfortunately, I **cannot execute any commands** in this environment — the sandbox is rejecting all shell execution with:

bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted


This means I cannot:
- Read the actual file contents
- Apply patches
- Run tests

Could you check if there's a sandbox configuration issue? Once commands can run, I can implement the fix directly. Alternatively, if you can share the contents of the two files, I can produce the exact patch needed.
Codex's Linux sandbox uses bubblewrap and needs access to create user namespaces.

対処方法

  • sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

以下で確認し 0 になっていれば OK です

  • sudo sysctl kernel.apparmor_restrict_unprivileged_userns
0

インスタンスの再起動などは不要です

最後に

danger-full-access は可能な限りやめましょう
名前的にもやばいオプション名なので

参考サイト

2026年7月3日金曜日

Codex + LiteLLM + カスタムOpenAIエンドポイントの設定方法

Codex + LiteLLM + カスタムOpenAIエンドポイントの設定方法

概要

素の OpenAI エンドポイントではなく特定の環境でラップされている URL の場合には LiteLLM を挟みましょう もしくは API が完全非互換でなかったり https プロトコルのみ提供されている場合なども LiteLLM を挟むと解決することがあります

環境

  • Ubuntu 24.04
  • codex 0.142.5
  • LiteLLM 1.90.0

LiteLLM 設定

api_base と api_key に自身の環境のカスタムOpenAIエンドポイントとカギを設定しましょう

  • vim litellm/litellm_config.yaml
model_list:
  - model_name: claude-chatAI
    litellm_params:
      model: anthropic/claude-sonnet-4-6
      api_base: https://your-api-endpoint
      api_key: xxx
      max_tokens: 4096
      temperature: 0.2

server_settings:
  port: 4000
  • vim litellm/compose.yaml
services:
  litellm:
    image: docker.litellm.ai/berriai/litellm:latest
    container_name: litellm
    ports:
      - "4000:4000"
    volumes:
      - ./litellm_config.yaml:/app/config.yaml
    restart: unless-stopped
    command: ["--config", "/app/config.yaml"]
  • docker compose up -d

codex 設定

base_url は LiteLLM が Listen しているアドレスで env_key は下記の SONNET_API_KEY を指定します
LiteLLM 側で認証情報を指定していますが一応 codex 側でも鍵情報を指定するためです

  • vim ~/.codex/litellm.config.toml
model_provider = "litellm"
model = "anthropic/claude-sonnet-4-6"
web_search = "disabled"

[model_providers.litellm]
name = "sonnet"
base_url = "http://localhost:4000"
env_key = "SONNET_API_KEY"
wire_api = "responses"

動作確認

  • export SONNET_API_KEY="xxx"
  • codex --profile litellm

これでインタラクティブモードで起動するので「test」など投げて応答がくれば OK です

最後に

たぶん OpenInterpreter も同じ方法でいけます
--full-auto オプションなどは廃止になっているの注意しましょう

参考サイト

https://docs.litellm.ai/docs/tutorials/openai_codex

2026年7月2日木曜日

OpenInterpreter や codex でカスタムエンドポイントURLを指定する方法

OpenInterpreter や codex でカスタムエンドポイントURLを指定する方法

概要

基本は config.toml に各方式になっています
環境変数 (OPENAI_BASE_URl)や --base_url は使いません

環境

  • OpenInterpreter 0.0.17
  • codex 0.142.5

~/.codex/mygpt.config.toml

model_provider = "mygpt"
model = "gpt-5.1"

[model_providers.mygpt]
name = "GPT5"
base_url = "https://your-api-endpoint"
env_key = "GPT5_API_KEY"
wire_api = "responses"

基本はプロファイルと後に作成します
config.toml は全体で共通の設定を記載するの個別のプロファイル別の情報は書きません

実行方法

env_key で指定した環境変数名に API キーを設定します

  • export GPT5_API_KEY="xxx"
  • codex --profile mygpt

最後に

codex の部分をそのまま openinterpreter に書き換えれば OpenInterpreter でも使えます

参考サイト