تتالي الوكلاء: الطرفية تختار ما يعمل منها فعلًا
حين تعطّل جزء من Claude وتوقفت بعض النماذج عن العمل، حاولت الانتقال إلى Gemini — فاكتشفت أن احتياطيّي معطّل منذ شهور. وهذه طريقة بناء احتياطي يصمد فعلًا.
TL;DR
مشغّل bash اسمه agent يختار أفضل أداة AI سطرية عاملة قبل أن يبدأ. يتدرّج على محورين: أولًا النموذج (Opus → Sonnet مع البقاء داخل Anthropic)، ثم طبقة النقل، وأخيرًا فقط المزوّد. كل فحص يجري استدعاءً حقيقيًا، لأن قوائم النماذج تكذب. وحارس يفحص الدرجات السفلى كل ساعتين وينبّه عبر Telegram.
$ agent --status
OK claude / claude-opus-5 api.anthropic.com reachable, claude-opus-5 answered
OK claude / claude-sonnet-5 api.anthropic.com reachable, claude-sonnet-5 answered
OK claude-proxy / claude-opus-5 claude-opus-5 answered
OK claude-proxy / claude-sonnet-5 claude-sonnet-5 answered
OK agy antigravity answered on Gemini 3.6 Flash (Low)
OK gemini localhost:8317 answered for gemini-3.1-pro-lowالمشكلة
حين لا يتوفر Claude أريد أن تستمر طرفيّتي في العمل. النسخة الساذجة هي «إذا سقط Claude فشغّل Gemini» — وهذا ما ظننت أنه لديّ. لكن الأعطال المثيرة ليست الانقطاعات. الأشيع: نفاد حصة Opus بينما يجيب Sonnet بلا مشكلة. القفز إلى مزوّد آخر هنا عبث.
إذن الاحتياطي الحقيقي يجب أن يتدرّج بالنموذج قبل أن يتدرّج بالمزوّد. وعليه أن يعرف أي درجة حيّة فعلًا — وهذا هو الجزء الصعب الذي أخطأت فيه مرتين.
كذبة الخمسة أشهر
قبل أن أكتب شيئًا فحصت ما لديّ. مجلد المصادقة في الوكيل روى القصة كاملة بختمين زمنيين:
claude-newiqa@gmail.com.json — جُدّد اليوم 06:41 · gemini-newiqa@gmail.com-….json — آخر لمسة في 27 مارس
رمز Claude كان يُجدَّد يوميًا. أما رمز Gemini فلم يتحرك منذ خمسة أشهر. فحص السلامة لديّ كان ينظر إلى claude-*.json فقط، فلم يلاحظ أحد. كان الاحتياطي زينةً منذ الربيع — ضوء أخضر فوق غرفة فارغة. الاحتياطي الذي لا يستدعيه أحد فعليًا ليس احتياطيًا.
لماذا لا تحل البوابة هذه المشكلة
الخطوة البديهية بوابة LLM محلية — Bifrost أو LiteLLM — مع تبديل المزوّد في الإعدادات. لديّ واحدة تعمل أصلًا (CLIProxyAPI)، فإضافة ثانية ما كانت لتجلب سوى خفيّ آخر يحتاج رعاية.
والأهم أن البوابة تفشل في المهمة الحقيقية. التبديل على مستوى الـ API يعني استبدال النموذج تحت العميل نفسه. أعطِ Claude Code ردًّا من Gemini عبر طبقة ترجمة فينهار عند tool-use والبث. على الاحتياطي أن يبدّل الأداة السطرية لا النموذج خلفها.
ست درجات ومحوران
المزوّد والنموذج يعطلان بشكل مستقل، لذا يشابك السلّم بينهما. كل درجة تشتري شيئًا محددًا، وثمن النزول مكتوب صراحة:
| Rung | What it survives | Cost of getting there |
|---|---|---|
| claude + opus | nothing — this is the happy path | — |
| claude + sonnet | Opus capped or overloaded | weaker model, same session shape |
| claude-proxy + opus | stale local login, broken CLI auth | separate OAuth token |
| claude-proxy + sonnet | both of the above at once | weaker model via proxy |
| agy | Anthropic outage / exhausted plan | different vendor, different CLI |
| gemini | agy itself broken | different CLI again, shares agy's quota |
في الكود الدرجة ثلاثية: الواجهة الخلفية، النموذج المراد فحصه، والنموذج المراد التشغيل به. النموذج الفارغ في الدرجة العليا أهم مما يبدو: عدم تمرير --model يحفظ opus[1m] من الملف الشخصي وسياقه البالغ مليون رمز.
# Each rung: backend | model to probe | model to run with
# An empty run-model means "let the CLI use its configured default", which keeps
# the profile's opus[1m] (and its 1M context) instead of downgrading it to plain
# opus just to name it explicitly.
RUNGS=(
"claude|$AGENT_OPUS_MODEL|"
"claude|$AGENT_SONNET_MODEL|sonnet"
"claude-proxy|$AGENT_OPUS_MODEL|$AGENT_OPUS_MODEL"
"claude-proxy|$AGENT_SONNET_MODEL|$AGENT_SONNET_MODEL"
"agy||"
"gemini||"
)حين لا تتوفر الدرجة العليا يُعلَن النزول بدل أن يحدث بصمت — يجب ألا تُخلط جلسة متدرّجة بجلسة عادية:
$ agent -p "explain this bug"
[agent] Claude Code (claude-opus-5) unavailable — claude-opus-5 unavailable: {"error":...}
[agent] using Claude Code (claude-sonnet-5)
# ...the session continues on Sonnet, inside Anthropic, with no vendor switch.قوائم النماذج تكذب
الغريزة الأولى فحص رخيص: اسأل البوابة عن /v1/models وتأكد أن النموذج موجود. هذا بلا قيمة. ظل وكيلي يعرض gemini-* بمرح طوال الوقت الذي كان فيه كل استدعاء يعيد auth_unavailable.
والأمر نفسه ينطبق على صحة النموذج. لا توجد نقطة نهاية للقوائم تخبرك أن Opus مقيّد الآن. الطلب الحقيقي وحده يُظهر ذلك — 429 أو 529:
# Real /v1/messages call for ONE model. This is what distinguishes "Opus is
# capped" from "Anthropic is down" — no model list can tell you that.
probe_model() {
local m="$1" body
body=$(curl -sS --max-time "$PROBE_TIMEOUT" \
"$AGENT_PROXY_URL/v1/messages" \
-H "Authorization: Bearer $AGENT_PROXY_KEY" \
-H 'content-type: application/json' \
-d "{\"model\":\"$m\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}")
case "$body" in
*'"type":"message"'*) REASON="$m answered"; return 0 ;;
esac
REASON="$m unavailable: $(printf '%s' "$body" | tr -d '\n' | cut -c1-140)"
return 1
}توفر النماذج للدرجات الرسمية مُستعار من الوكيل الذي يواجه الحساب نفسه لدى Anthropic — فالنموذج المستنفَد هناك مستنفَد في الأداة الرسمية أيضًا. وقد قيس الفحص المسبق كله عند 7 رموز ونحو 1.3 ثانية: فحص حيوية مجاني بلا مصادقة يعيد 401، إضافة إلى استدعاء واحد بحجم رمز واحد، مع تخزين نتيجة Opus مؤقتًا فتكلّف درجتان فحصًا واحدًا.
شبكتان لا واحدة
الفحص المسبق لا يلتقط كل شيء. إذا نفدت الحصة في الفجوة بين الفحص والتشغيل، كان الفحص محقًّا وبلا فائدة رغم ذلك. لذا هناك شبكة ثانية: رمز الخروج مع الزمن المنقضي.
start=$SECONDS
run_rung "$backend" "$rmodel" "$@"
rc=$?
elapsed=$(( SECONDS - start ))
# Clean exit, or the user interrupted it — either way, done.
if [ $rc -eq 0 ] || [ $rc -eq 130 ]; then
exit $rc
fi
# Survived long enough to have been genuinely used: a real error, not a rung
# that never started. Do not silently rerun the work somewhere else.
if [ $elapsed -ge $FASTFAIL_SECONDS ]; then
exit $rc
fi
warn "$(label "$backend" "$pmodel") exited $rc after ${elapsed}s — treating as unavailable"عتبة الـ25 ثانية تُرمّز حكمًا. الدرجة التي تموت فورًا لم تبدأ حقًا. أما التي عملت دقائق ثم فشلت فقد كانت تعمل فعلًا، وإعادة ذلك العمل بصمت لدى مزوّد آخر أسوأ من إظهار الخطأ.
الحارس والفخّ الذي بداخله
المشغّل لا يعمل إلا حين أشغّله، فقد تتعفّن درجة بين الجلسات. فحص مدمج في droid السلامة لديّ (launchd، كل ساعتين، تنبيهات Telegram) يستدعي شيفرة الفحص نفسها. نسختي الأولى حملت الخلل الذي يدور حوله هذا المقال بالضبط: كانت تُرجع نجاحًا عند أول درجة خضراء.
# Healthy — every non-Anthropic rung answers
$ agent --check-fallback
all provider-independent fallbacks OK: agy — antigravity answered...; gemini — ...
rc=0
# One rung rotted while the other still works. This is the case that used to
# pass silently, and the whole reason the check exists.
$ GEMINI_API_KEY=broken agent --check-fallback
DEGRADED: still covered by agy — antigravity answered on Gemini 3.6 Flash (Low)
but a rung died: gemini — gemini call failed: {"error":"Invalid API key"}
rc=1وهذا خطأ، لأن الدرجتين غير التابعتين لـ Anthropic تحملان رمزين منفصلين — لأداة Antigravity رمزها الخاص، بينما تستخدم درجة Gemini رمز الوكيل. قد تتعفن إحداهما بينما تبدو الأخرى مثالية. لذا يفحص الفاحص كل درجة ويعامل DEGRADED كفشل أيضًا.
ثلاثة أخطاء تستحق التدوين
كل واحد منها أنتج إشارة خضراء فوق آلة معطوبة — النمط نفسه ثلاث مرات في أصيل واحد:
- الفحص اختبر نموذجًا لا أستخدمه أبدًا. فحصت
gemini-3.1-pro-previewبينما كانت الأداة مضبوطة علىgemini-3.1-pro-low. النموذجان يُحلّان إلى مزوّدين مختلفين، فشخّصت «رمزًا منتهيًا» لم يوجد أصلًا. على الفحص أن يسلك المسار ذاته الذي يسلكه العميل الحقيقي. - متغيّر فارغ بصمت. تحليل مفتاح YAML للوكيل عبر
awkو\x27للاقتباس أعطى سلسلة فارغة على BSD awk — بلا خطأ ولا تحذير. ومع ذلك نجح الفحص. أُصلح بتمرير الاقتباس عبرawk -v q="'". - نجح الفحص بينما فشل الشيء الحقيقي. أعاد فحص
curlرمز 200، لكنgemini -pالحقيقي رفض الإقلاع: الوضع بلا واجهة يتوقف عند المجلدات غير الموثوقة. كانت الدرجة معطوبة تحديدًا في الحالة التي وُجدت من أجلها.
الاستخدام
كتابة agent تمنحك أفضل درجة متاحة بتجربة Claude Code المعتادة. الاختيار الصريح يتخطى الفحص تمامًا — إن طلبت Gemini حصلت على Gemini:
agent # auto-pick the best live rung
agent --use gemini # force a backend
agent --use claude:sonnet # force a backend AND a model
agent --use claude-proxy:opus # opus/sonnet/haiku map to full ids per backend
agent --list # backend names
agent --status # probe every rung and report
agent --check-fallback # exit 1 if a non-Anthropic rung is missing
agent -p "..." # flags pass straight through to the chosen CLIالخلاصة
الهندسة هنا عادية: سكربت bash، بضعة استدعاءات curl، ومصفوفة مرتبة. ما تطلّب الجهد هو عدم الثقة بفحوصي أنا. ثلاث مرات بنيت شيئًا يبلّغ عن النجاح بينما ما تحته معطوب — وفي كل مرة كان الفحص الرخيص السريع هو المذنب.
المتانة ليست قائمة الواجهات الخلفية التي أعددتها، بل وجود ما يتحقق من أن القائمة ما زالت صحيحة. قائمتي كانت ست درجات على الورق ودرجة واحدة حقيقية طوال خمسة أشهر.
المصادر
- Claude Code Documentation — headless mode,
ANTHROPIC_BASE_URL, model flags - CLIProxyAPI — the local multi-provider gateway behind the proxy rungs
- Gemini CLI — the last rung, and its trusted-folders behaviour
- Apple Developer — launchd jobs, used by the watchdog
لاحظت خطأً؟
معلومة خاطئة، ترجمة ركيكة، أو شيء يبدو غير صحيح في هذه المقالة؟ راسلني — بلغتك أنت.