feat(ai): add guarded Claude fallback route
This commit is contained in:
@@ -8,8 +8,8 @@ Telegram /ai 指令動態控制 AI Router 狀態
|
||||
Redis Keys:
|
||||
ai:control:use_router "true"/"false" (TTL: 30天)
|
||||
ai:control:primary_provider legacy compatibility; only "ollama"/"ollama_gcp_a"
|
||||
ai:control:disabled:<provider> "1" disabled / Gemini "0" enabled
|
||||
(Gemini 無 TTL;其他 provider 30 天)
|
||||
ai:control:disabled:<provider> "1" disabled / paid provider "0" enabled
|
||||
(paid providers 無 TTL;free providers 30 天)
|
||||
|
||||
Usage:
|
||||
/ai status — 顯示所有 Provider 狀態 + 當前路由模式
|
||||
@@ -31,19 +31,20 @@ _PRIMARY_PROVIDER_KEY = "ai:control:primary_provider"
|
||||
_DISABLED_KEY_PREFIX = "ai:control:disabled:"
|
||||
|
||||
# Free-provider/router compatibility controls retain the historical 30-day
|
||||
# expiry. Gemini's explicit paid-provider state is intentionally persistent.
|
||||
# expiry. Paid-provider explicit control state is intentionally persistent.
|
||||
_CONTROL_KEY_TTL = 30 * 24 * 3600 # 30 days
|
||||
PAID_PROVIDERS = {"claude", "gemini"}
|
||||
|
||||
VALID_PROVIDERS = {
|
||||
"ollama",
|
||||
"ollama_gcp_a",
|
||||
"ollama_gcp_b",
|
||||
"ollama_local",
|
||||
"claude",
|
||||
"gemini",
|
||||
}
|
||||
VALID_PRIMARY_PROVIDERS = {"ollama", "ollama_gcp_a"}
|
||||
SHADOW_METADATA_ONLY_PROVIDERS = {
|
||||
"claude",
|
||||
"nvidia",
|
||||
"nemotron",
|
||||
"openclaw",
|
||||
@@ -130,15 +131,15 @@ async def is_provider_disabled(provider: str) -> bool:
|
||||
# durable control key is disabled until an operator explicitly enables
|
||||
# it; free Ollama lanes retain their historical missing-key=enabled
|
||||
# behaviour.
|
||||
if provider == "gemini":
|
||||
if provider in PAID_PROVIDERS:
|
||||
return val is None or (
|
||||
val.decode() if isinstance(val, bytes) else str(val)
|
||||
) != "0"
|
||||
return val is not None
|
||||
except Exception:
|
||||
# Paid-provider disable truth is fail-closed. Callers can catch this to
|
||||
# publish an unavailable readback, but must never re-add Gemini.
|
||||
if provider == "gemini":
|
||||
# publish an unavailable readback, but must never re-add a paid lane.
|
||||
if provider in PAID_PROVIDERS:
|
||||
raise
|
||||
return False
|
||||
|
||||
@@ -151,13 +152,13 @@ async def set_provider_disabled(provider: str, disabled: bool) -> bool:
|
||||
r = await _get_redis()
|
||||
key = f"{_DISABLED_KEY_PREFIX}{provider}"
|
||||
if disabled:
|
||||
if provider == "gemini":
|
||||
if provider in PAID_PROVIDERS:
|
||||
# Paid-provider disable truth must not silently expire.
|
||||
await r.set(key, "1")
|
||||
else:
|
||||
await r.set(key, "1", ex=_CONTROL_KEY_TTL)
|
||||
else:
|
||||
if provider == "gemini":
|
||||
if provider in PAID_PROVIDERS:
|
||||
# Explicitly persist the enabled state. Deleting the key would
|
||||
# fail closed to disabled on the next read.
|
||||
await r.set(key, "0")
|
||||
@@ -190,7 +191,13 @@ async def get_status_summary() -> str:
|
||||
|
||||
# Provider 狀態
|
||||
provider_lines = []
|
||||
for p in ["ollama_gcp_a", "ollama_gcp_b", "ollama_local", "gemini"]:
|
||||
for p in [
|
||||
"ollama_gcp_a",
|
||||
"ollama_gcp_b",
|
||||
"ollama_local",
|
||||
"claude",
|
||||
"gemini",
|
||||
]:
|
||||
try:
|
||||
disabled = await is_provider_disabled(p)
|
||||
except Exception:
|
||||
@@ -206,7 +213,7 @@ async def get_status_summary() -> str:
|
||||
cost_lines = []
|
||||
try:
|
||||
r = await _get_redis()
|
||||
for p in ["gemini"]:
|
||||
for p in ["claude", "gemini"]:
|
||||
val = await r.get(f"ai_rate:total_cost:{p}")
|
||||
if val:
|
||||
cost = float(val)
|
||||
@@ -303,7 +310,7 @@ async def handle_ai_command(text: str) -> str:
|
||||
try:
|
||||
r = await _get_redis()
|
||||
total = 0.0
|
||||
for p in ["gemini"]:
|
||||
for p in ["claude", "gemini"]:
|
||||
val = await r.get(f"ai_rate:total_cost:{p}")
|
||||
if val:
|
||||
cost = float(val)
|
||||
@@ -329,7 +336,7 @@ async def handle_ai_command(text: str) -> str:
|
||||
"<code>/ai disable <p></code> — 停用 Provider\n"
|
||||
"<code>/ai cost</code> — 費用統計\n\n"
|
||||
f"可控 Provider: {', '.join(sorted(VALID_PROVIDERS))}\n"
|
||||
"Legacy providers 僅保留 shadow metadata,不可執行"
|
||||
"Legacy providers 僅保留 shadow metadata;Claude/Gemini 受 persistent paid gate 控制"
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user