Skip to main content
العودة إلى المدونة
Claude CodeAICLIBashAutomationResilience

تتالي الوكلاء: الطرفية تختار ما يعمل منها فعلًا

حين تعطّل جزء من Claude وتوقفت بعض النماذج عن العمل، حاولت الانتقال إلى Gemini — فاكتشفت أن احتياطيّي معطّل منذ شهور. وهذه طريقة بناء احتياطي يصمد فعلًا.

نُشر 29 أغسطس 20269 دقيقة قراءة

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 والبث. على الاحتياطي أن يبدّل الأداة السطرية لا النموذج خلفها.

ست درجات ومحوران

المزوّد والنموذج يعطلان بشكل مستقل، لذا يشابك السلّم بينهما. كل درجة تشتري شيئًا محددًا، وثمن النزول مكتوب صراحة:

RungWhat it survivesCost of getting there
claude + opusnothing — this is the happy path
claude + sonnetOpus capped or overloadedweaker model, same session shape
claude-proxy + opusstale local login, broken CLI authseparate OAuth token
claude-proxy + sonnetboth of the above at onceweaker model via proxy
agyAnthropic outage / exhausted plandifferent vendor, different CLI
geminiagy itself brokendifferent CLI again, shares agy's quota

في الكود الدرجة ثلاثية: الواجهة الخلفية، النموذج المراد فحصه، والنموذج المراد التشغيل به. النموذج الفارغ في الدرجة العليا أهم مما يبدو: عدم تمرير --model يحفظ opus[1m] من الملف الشخصي وسياقه البالغ مليون رمز.

agent
# 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:

agent
# 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 مؤقتًا فتكلّف درجتان فحصًا واحدًا.

شبكتان لا واحدة

الفحص المسبق لا يلتقط كل شيء. إذا نفدت الحصة في الفجوة بين الفحص والتشغيل، كان الفحص محقًّا وبلا فائدة رغم ذلك. لذا هناك شبكة ثانية: رمز الخروج مع الزمن المنقضي.

agent
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، ومصفوفة مرتبة. ما تطلّب الجهد هو عدم الثقة بفحوصي أنا. ثلاث مرات بنيت شيئًا يبلّغ عن النجاح بينما ما تحته معطوب — وفي كل مرة كان الفحص الرخيص السريع هو المذنب.

المتانة ليست قائمة الواجهات الخلفية التي أعددتها، بل وجود ما يتحقق من أن القائمة ما زالت صحيحة. قائمتي كانت ست درجات على الورق ودرجة واحدة حقيقية طوال خمسة أشهر.

المصادر

لاحظت خطأً؟

معلومة خاطئة، ترجمة ركيكة، أو شيء يبدو غير صحيح في هذه المقالة؟ راسلني — بلغتك أنت.