feat(ai): add guarded Claude fallback route
This commit is contained in:
@@ -4,7 +4,7 @@ AWOOOI API Configuration
|
||||
Pydantic Settings + Environment Variables
|
||||
|
||||
ADR-005: BFF Architecture
|
||||
ADR-006: AI Fallback Strategy (GCP-A -> GCP-B -> host111 -> Gemini)
|
||||
ADR-006: AI Fallback Strategy (GCP-A -> GCP-B -> host111 -> Claude -> Gemini)
|
||||
|
||||
Four Iron Laws:
|
||||
1. Async-First
|
||||
@@ -428,8 +428,8 @@ class Settings(BaseSettings):
|
||||
default=["bge-m3:latest", "qwen2.5:7b-instruct", "qwen3:14b", "deepseek-r1:14b", "hermes3:latest"],
|
||||
description="HeartbeatReportService 探測必要模型是否載入",
|
||||
)
|
||||
# Gemini is the final paid fallback after GCP-A -> GCP-B -> host111.
|
||||
# All generation calls atomically reserve requests/tokens/cost in Redis.
|
||||
# Claude and Gemini are paid fallbacks after GCP-A -> GCP-B -> host111.
|
||||
# All paid generation calls atomically reserve requests/tokens/cost in Redis.
|
||||
GEMINI_MODEL: str = Field(
|
||||
default="gemini-2.5-flash-lite",
|
||||
min_length=1,
|
||||
@@ -486,6 +486,78 @@ class Settings(BaseSettings):
|
||||
le=3600,
|
||||
description="Gemini failed generation work-item cooldown seconds",
|
||||
)
|
||||
CLAUDE_MODEL: str = Field(
|
||||
default="claude-sonnet-5",
|
||||
min_length=1,
|
||||
description=(
|
||||
"Anthropic SRE/architecture paid fallback model; exact model must "
|
||||
"have a reviewed pricing policy before any generation"
|
||||
),
|
||||
)
|
||||
CLAUDE_DAILY_QUOTA: int = Field(
|
||||
default=50,
|
||||
ge=1,
|
||||
description="Claude daily generation request hard limit",
|
||||
)
|
||||
CLAUDE_RPM_LIMIT: int = Field(
|
||||
default=2,
|
||||
ge=1,
|
||||
description="Claude generation requests per minute hard limit",
|
||||
)
|
||||
CLAUDE_DAILY_TOKEN_LIMIT: int = Field(
|
||||
default=100_000,
|
||||
ge=1,
|
||||
description="Claude daily token hard limit including pending reservations",
|
||||
)
|
||||
CLAUDE_DAILY_COST_LIMIT_USD: float = Field(
|
||||
default=1.0,
|
||||
gt=0,
|
||||
description="Claude daily spend hard limit in USD",
|
||||
)
|
||||
CLAUDE_TOTAL_COST_LIMIT_USD: float = Field(
|
||||
default=5.0,
|
||||
gt=0,
|
||||
description="Claude cumulative spend hard limit in USD",
|
||||
)
|
||||
CLAUDE_COST_ALERT_THRESHOLD_USD: float = Field(
|
||||
default=4.0,
|
||||
gt=0,
|
||||
description="Claude cumulative spend warning threshold in USD",
|
||||
)
|
||||
CLAUDE_MAX_OUTPUT_TOKENS: int = Field(
|
||||
default=4096,
|
||||
ge=1,
|
||||
le=16_384,
|
||||
description="Claude output ceiling and pre-call cost reservation basis",
|
||||
)
|
||||
CLAUDE_USAGE_RECEIPT_TTL_SECONDS: int = Field(
|
||||
default=604_800,
|
||||
ge=86_400,
|
||||
description=(
|
||||
"Claude non-secret auth-status receipt TTL; generation and run "
|
||||
"identity receipts remain durable"
|
||||
),
|
||||
)
|
||||
CLAUDE_FAILURE_COOLDOWN_SECONDS: int = Field(
|
||||
default=120,
|
||||
ge=1,
|
||||
le=3600,
|
||||
description="Claude failed generation work-item cooldown seconds",
|
||||
)
|
||||
CLAUDE_EXECUTION_MODE: str = Field(
|
||||
default="shadow",
|
||||
pattern="^(shadow|canary|production)$",
|
||||
description=(
|
||||
"Claude route mode: shadow performs no API call; canary uses a "
|
||||
"deterministic run bucket; production enables the ordered fallback"
|
||||
),
|
||||
)
|
||||
CLAUDE_CANARY_PERCENT: int = Field(
|
||||
default=0,
|
||||
ge=0,
|
||||
le=100,
|
||||
description="Deterministic Claude canary percentage when mode=canary",
|
||||
)
|
||||
# Deprecated: use OPENCLAW_URL instead
|
||||
CLAWBOT_URL: str = Field(
|
||||
default="http://192.168.0.188:8088", # 🔧 修正: OpenClaw 實際 port 是 8088
|
||||
@@ -588,14 +660,25 @@ class Settings(BaseSettings):
|
||||
|
||||
# ==========================================================================
|
||||
# Runtime fallback strategy. Alert/incident order is enforced as:
|
||||
# GCP-A -> GCP-B -> host111 Ollama -> Gemini. No paid/provider insertion.
|
||||
# GCP-A -> GCP-B -> host111 Ollama -> Claude -> Gemini.
|
||||
# ==========================================================================
|
||||
AI_FALLBACK_ORDER: list[str] = Field(
|
||||
default=["ollama_gcp_a", "ollama_gcp_b", "ollama_local", "gemini"],
|
||||
default=[
|
||||
"ollama_gcp_a",
|
||||
"ollama_gcp_b",
|
||||
"ollama_local",
|
||||
"claude",
|
||||
"gemini",
|
||||
],
|
||||
description=(
|
||||
"唯一 production provider chain: GCP-A -> GCP-B -> host111 -> Gemini"
|
||||
"唯一 production provider chain: GCP-A -> GCP-B -> host111 -> "
|
||||
"Claude -> Gemini"
|
||||
),
|
||||
)
|
||||
ENABLE_CLAUDE: bool = Field(
|
||||
default=False,
|
||||
description="Claude paid fallback explicit opt-in; default disabled",
|
||||
)
|
||||
ENABLE_GEMINI: bool = Field(
|
||||
default=False,
|
||||
description="Gemini paid final fallback explicit opt-in; default disabled",
|
||||
@@ -615,30 +698,30 @@ class Settings(BaseSettings):
|
||||
description=(
|
||||
"Allow incident/alert OpenClaw analysis to use cloud fallback "
|
||||
"providers after the GCP-A/GCP-B/111 Ollama lane is exhausted. "
|
||||
"Default true so Gemini can act as the final backup, after the "
|
||||
"ordered Ollama lane is exhausted."
|
||||
"Default true so Claude and then Gemini can act as paid backups "
|
||||
"after the ordered Ollama lane is exhausted."
|
||||
),
|
||||
)
|
||||
ALERT_AI_ENFORCE_OLLAMA_FIRST: bool = Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Force incident/alert OpenClaw analysis to try GCP-A, then GCP-B, "
|
||||
"then local 111 before cloud backup providers such as Gemini."
|
||||
"then local 111 before cloud backup providers Claude and Gemini."
|
||||
),
|
||||
)
|
||||
ALERT_AI_STRICT_PROVIDER_CHAIN: bool = Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Forbid direct OpenClaw/Nemo/NVIDIA/Claude incident delegation; "
|
||||
"the only alert route is GCP-A, GCP-B, host111, then Gemini."
|
||||
"Forbid direct OpenClaw/Nemo/NVIDIA incident delegation; the only "
|
||||
"alert route is GCP-A, GCP-B, host111, Claude, then Gemini."
|
||||
),
|
||||
)
|
||||
ALERT_OLLAMA_MODEL: str = Field(
|
||||
default="qwen3:14b",
|
||||
description=(
|
||||
"Ollama model used for incident/alert deep diagnosis. Alert cards "
|
||||
"may wait for this model; Gemini remains a backup after GCP-A, "
|
||||
"GCP-B, and 111 fail."
|
||||
"may wait for this model; Claude then Gemini remain paid backups "
|
||||
"after GCP-A, GCP-B, and 111 fail."
|
||||
),
|
||||
)
|
||||
INCIDENT_LLM_TIMEOUT_SECONDS: int = Field(
|
||||
@@ -670,9 +753,9 @@ class Settings(BaseSettings):
|
||||
def parse_ai_fallback(cls, v: str | list[str]) -> list[str]:
|
||||
"""
|
||||
解析 AI_FALLBACK_ORDER,支援三種格式:
|
||||
1. JSON: '["ollama_gcp_a","ollama_gcp_b","ollama_local","gemini"]'
|
||||
2. CSV: 'ollama_gcp_a,ollama_gcp_b,ollama_local,gemini'
|
||||
3. List: ["ollama_gcp_a", "ollama_gcp_b", "ollama_local", "gemini"]
|
||||
1. JSON: '["ollama_gcp_a","ollama_gcp_b","ollama_local","claude","gemini"]'
|
||||
2. CSV: 'ollama_gcp_a,ollama_gcp_b,ollama_local,claude,gemini'
|
||||
3. List: ["ollama_gcp_a", "ollama_gcp_b", "ollama_local", "claude", "gemini"]
|
||||
|
||||
2026-03-27 修復: ConfigMap 用 JSON 格式,原本只支援 CSV
|
||||
"""
|
||||
@@ -692,13 +775,19 @@ class Settings(BaseSettings):
|
||||
else:
|
||||
values = [p.strip().lower() for p in v]
|
||||
|
||||
expected = ["ollama_gcp_a", "ollama_gcp_b", "ollama_local", "gemini"]
|
||||
if values == ["ollama", "gemini"]:
|
||||
expected = [
|
||||
"ollama_gcp_a",
|
||||
"ollama_gcp_b",
|
||||
"ollama_local",
|
||||
"claude",
|
||||
"gemini",
|
||||
]
|
||||
if values in (["ollama", "gemini"], ["ollama", "claude", "gemini"]):
|
||||
return expected
|
||||
if values != expected:
|
||||
raise ValueError(
|
||||
"AI_FALLBACK_ORDER must be exactly "
|
||||
"ollama_gcp_a,ollama_gcp_b,ollama_local,gemini"
|
||||
"ollama_gcp_a,ollama_gcp_b,ollama_local,claude,gemini"
|
||||
)
|
||||
return values
|
||||
|
||||
|
||||
Reference in New Issue
Block a user