Merge remote-tracking branch 'origin/main' into codex/p0-obs-002-20260715

This commit is contained in:
ogt
2026-07-15 01:22:17 +08:00
71 changed files with 6431 additions and 2230 deletions

View File

@@ -4,7 +4,7 @@ AWOOOI API Configuration
Pydantic Settings + Environment Variables
ADR-005: BFF Architecture
ADR-006: AI Fallback Strategy (Ollama -> Gemini -> Claude)
ADR-006: AI Fallback Strategy (GCP-A -> GCP-B -> host111 -> Gemini)
Four Iron Laws:
1. Async-First
@@ -418,12 +418,63 @@ class Settings(BaseSettings):
default=["bge-m3:latest", "qwen2.5:7b-instruct", "qwen3:14b", "deepseek-r1:14b", "hermes3:latest"],
description="HeartbeatReportService 探測必要模型是否載入",
)
# 2026-04-25 critic-fix Part2 H7 by Claude Engineer-C2
# Gemini 帳單熔斷:每日呼叫上限,超過改走 188+Nemotron
# 超過上限後寫 Redis key ollama:gemini_daily_count:{date}TTL 86400s
# Gemini is the final paid fallback after GCP-A -> GCP-B -> host111.
# All generation calls atomically reserve requests/tokens/cost in Redis.
GEMINI_MODEL: str = Field(
default="gemini-2.5-flash-lite",
min_length=1,
description=(
"Gemini 最終付費備援模型;成本閘只允許具有已查核 pricing policy 的模型"
),
)
GEMINI_DAILY_QUOTA: int = Field(
default=1000,
description="每日 Gemini 呼叫上限,超過切到 188+NemotronP1.1 帳單熔斷)",
default=500,
ge=1,
description="Gemini 每日 generation request 上限(單一權威值)",
)
GEMINI_RPM_LIMIT: int = Field(
default=10,
ge=1,
description="Gemini 每分鐘 generation request 上限",
)
GEMINI_DAILY_TOKEN_LIMIT: int = Field(
default=100_000,
ge=1,
description="Gemini 每日 token 上限(含尚未完成的原子 reservation",
)
GEMINI_DAILY_COST_LIMIT_USD: float = Field(
default=5.0,
gt=0,
description="Gemini 單日成本硬上限USD",
)
GEMINI_TOTAL_COST_LIMIT_USD: float = Field(
default=5.0,
gt=0,
description="Gemini 累積成本硬上限USD",
)
GEMINI_COST_ALERT_THRESHOLD_USD: float = Field(
default=4.0,
gt=0,
description="Gemini 累積成本告警閾值USD必須不高於硬上限",
)
GEMINI_MAX_OUTPUT_TOKENS: int = Field(
default=2048,
ge=1,
description="Gemini generation output 上限與 pre-call token reservation 基準",
)
GEMINI_USAGE_RECEIPT_TTL_SECONDS: int = Field(
default=604_800,
ge=86_400,
description=(
"Gemini non-secret auth-status receipt 保存秒數(預設七日);"
"generation receipt 與 run identity 不設 TTL"
),
)
GEMINI_FAILURE_COOLDOWN_SECONDS: int = Field(
default=60,
ge=1,
le=3600,
description="Gemini failed generation work-item cooldown seconds",
)
# Deprecated: use OPENCLAW_URL instead
CLAWBOT_URL: str = Field(
@@ -522,15 +573,18 @@ class Settings(BaseSettings):
)
# ==========================================================================
# AI Fallback Strategy (ADR-006 v1.3 + ADR-036)
# Order: Ollama (local) -> NVIDIA NIM -> Gemini (cloud) -> Claude (cloud)
# Tool Calling: Nemotron (專用) -> Gemini -> Claude
# 2026-04-28 ogt + Claude Opus 4.7: 補 nvidia 對齊 ConfigMap (k8s/awoooi-prod/04-configmap.yaml)
# 之前 default 缺 nvidia 與 ConfigMap driftfeedback_ai_fallback_order.md 鐵律
# Runtime fallback strategy. Alert/incident order is enforced as:
# GCP-A -> GCP-B -> host111 Ollama -> Gemini. No paid/provider insertion.
# ==========================================================================
AI_FALLBACK_ORDER: list[str] = Field(
default=["ollama", "nvidia", "gemini", "claude"],
description="AI provider fallback order",
default=["ollama_gcp_a", "ollama_gcp_b", "ollama_local", "gemini"],
description=(
"唯一 production provider chain: GCP-A -> GCP-B -> host111 -> Gemini"
),
)
ENABLE_GEMINI: bool = Field(
default=False,
description="Gemini paid final fallback explicit opt-in; default disabled",
)
GEMINI_API_KEY: str = Field(default="", description="Google Gemini API key")
CLAUDE_API_KEY: str = Field(default="", description="Anthropic Claude API key")
@@ -558,6 +612,13 @@ class Settings(BaseSettings):
"then local 111 before cloud backup providers such as 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."
),
)
ALERT_OLLAMA_MODEL: str = Field(
default="qwen3:14b",
description=(
@@ -595,9 +656,9 @@ class Settings(BaseSettings):
def parse_ai_fallback(cls, v: str | list[str]) -> list[str]:
"""
解析 AI_FALLBACK_ORDER支援三種格式:
1. JSON: '["gemini","ollama","claude"]'
2. CSV: 'gemini,ollama,claude'
3. List: ["gemini", "ollama", "claude"]
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"]
2026-03-27 修復: ConfigMap 用 JSON 格式,原本只支援 CSV
"""
@@ -609,12 +670,23 @@ class Settings(BaseSettings):
if v.startswith("["):
try:
parsed = json.loads(v)
return [p.strip().lower() for p in parsed]
values = [p.strip().lower() for p in parsed]
except json.JSONDecodeError:
pass # 降級到 CSV 解析
# CSV 格式
return [provider.strip().lower() for provider in v.split(",")]
return [p.lower() for p in v]
values = [provider.strip().lower() for provider in v.split(",")]
else:
values = [provider.strip().lower() for provider in v.split(",")]
else:
values = [p.strip().lower() for p in v]
expected = ["ollama_gcp_a", "ollama_gcp_b", "ollama_local", "gemini"]
if values == ["ollama", "gemini"]:
return expected
if values != expected:
raise ValueError(
"AI_FALLBACK_ORDER must be exactly "
"ollama_gcp_a,ollama_gcp_b,ollama_local,gemini"
)
return values
# ==========================================================================
# Kubernetes / K3s (CTO-201)