I was reading the Gemini API changelog for October 27 when a new line stopped me: a command-line client called gemini-api, in beta.
The names are close, and both call Google models. Should I split work that agy -p already handles into a second command? Or, the other way round, is it wise to let agy, which can touch a repository, handle requests that only need reading? As an indie developer reviewing my own work with nobody else in the room, I wanted the line drawn before I started using either.
One thing up front. What I verified by hand is the branching of a routing script. gemini-api is in beta and I did not pull its binary; I tested with fake commands that record their arguments and return an exit code. Everything I say about gemini-api itself rests on the official documentation.
What the docs say about gemini-api
From the Gemini API CLI guide and the October 27 changelog, only the parts that matter for routing:
- It is described as a stateless, zero-dependency client that calls Gemini models and managed agents through the Interactions API
- It is an open-source beta, and breaking changes can land between minor versions. Pin a release in automated environments
analyze -i <file>reads PDFs, images, audio, video, CSV and text. Inline input is capped at 14 MB of raw data; beyond that, usefiles upload. Uploaded files are deleted after 48 hours- Exit codes: 0 success, 1 runtime or API error, 2 usage or validation error, 3 authentication or authorization failure
generatestreams to stdout by default; combine it with-o jsonor--jqonly with--stream=false
On the other side, agy -p is the headless mode: run one prompt and exit. Operations that need approval become soft-denies, and the exit code stays 0 while a notice goes to stderr. I wrote that up in an earlier article.
The line: does it write, or only read?
My first idea was to classify the prompt automatically by keywords such as "fix". That falls apart quickly: "tell me whether this table needs fixing" is a read-only request that would be routed to the side that can edit files. It is easier to trace an incident later if the caller declares intent in one word.
So the front door has three kinds: edit, analyze, ask.
| Kind | Goes to | Use it for |
|---|---|---|
edit | agy -p | Requests that read and write files in the repository |
analyze | gemini-api analyze | Read one file, return just an answer |
ask | gemini-api generate | Stateless one-off questions |
The 54-line script
#!/usr/bin/env bash
# route.sh — 依頼の種類を見て agy と gemini-api を振り分ける小さな玄関
set -uo pipefail
LIMIT="${ROUTE_TIMEOUT:-5m}"
INLINE_MAX=$((14 * 1024 * 1024)) # gemini-api の -i は生データ 14MB まで
die() { echo "route: $1" >&2; exit "${2:-64}"; }
need() { command -v "$1" >/dev/null 2>&1 || die "$1 が見つかりません" 127; }
kind="${1:-}"; [ -n "$kind" ] || die "使い方: route.sh edit|analyze|ask ..."
shift
case "$kind" in
edit)
# リポジトリを読み書きさせたい依頼は agy に渡す
need agy
[ "$#" -gt 0 ] || die "依頼文がありません"
timeout "$LIMIT" agy -p "$*"
rc=$?
;;
analyze)
# 1 つのファイルを読ませて答えだけ欲しい依頼は gemini-api に渡す
need gemini-api
file="${1:-}"; shift || true
[ -f "$file" ] || die "ファイルがありません: $file"
[ "$#" -gt 0 ] || die "質問文がありません"
size=$(wc -c < "$file")
if [ "$size" -gt "$INLINE_MAX" ]; then
uri=$(timeout "$LIMIT" gemini-api files upload "$file" --wait) || die "アップロードに失敗しました" 1
input="$uri"
else
input="$file"
fi
timeout "$LIMIT" gemini-api analyze -i "$input" --stream=false "$*"
rc=$?
;;
ask)
# 状態を持たない単発の質問は gemini-api の generate
need gemini-api
[ "$#" -gt 0 ] || die "質問文がありません"
timeout "$LIMIT" gemini-api generate --stream=false "$*"
rc=$?
;;
*) die "未知の種類です: $kind" ;;
esac
case "$rc" in
0) ;;
124) echo "route: ${LIMIT} で打ち切りました" >&2 ;;
2) echo "route: 引数の組み立てが誤っています(終了コード 2)" >&2 ;;
3) echo "route: 認証に失敗しました。GEMINI_API_KEY か whoami を確認してください" >&2 ;;
esac
exit "$rc"Three details matter.
First, the 14 MB line. analyze measures the file and only when it exceeds the ceiling does it call files upload --wait and pass the returned files/<id> to -i. Uploading small files would just pile up temporary files that vanish after 48 hours.
Second, timeout wraps the whole call. A timed-out run exits with timeout's 124, which belongs to neither CLI, so it never collides with the documented 0-3.
Third, exit codes are never swallowed. Only 2 and 3 get a one-line hint, and the original code goes back to the caller.
The branches I actually checked
I put a fake agy and a fake gemini-api at the front of PATH, logged their arguments, and confirmed the following.
| Call | Result |
|---|---|
edit "..." | agy -p ... called once, exit 0 |
analyze on a small file | Only analyze -i a.txt --stream=false ... |
analyze on a ~15 MB file | files upload big.bin --wait, then -i files/... |
| Fake CLI returns 3 | Authentication hint, exit 3 |
| Fake CLI returns 2 | Usage hint, exit 2 |
Fake agy sleeping 5 s with ROUTE_TIMEOUT=1s | Timeout notice, exit 124 |
| Unknown kind / missing command | 64 / 127 |
Caveats and what I have not checked
- I have not run the real
gemini-api. It is a beta, so flag names may change. Checkgemini-api --usagebefore relying on this - The official examples include installing with
curl ... | bash. I would read the script first, or use a pinned release archive - An exit code of 0 from
agyincludes the soft-deny case. Foredit, judge the result by the diff, not by the exit code alone - Use a dummy such as
YOUR_API_KEYand pass the real key throughGEMINI_API_KEY. Never write it into the script
One next step
Writes go to agy, reads go to gemini-api, and when I am unsure, the caller says which in one word. I intend to keep revisiting that line as more beta CLIs appear.
To begin, pull the last five agy -p calls from your shell history and sort them into "asked it to change something" and "only wanted an answer". I'd rather start with that check, because it costs the least rework.