Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c9fc649
feat: add centralized AI budget guard
Pigbibi Jul 11, 2026
0dedcf8
fix: enforce budget reservations through gateway
Pigbibi Jul 11, 2026
a32152e
fix: close reservation lifecycle gaps
Pigbibi Jul 11, 2026
6122b6a
fix: reconcile settled budget usage safely
Pigbibi Jul 11, 2026
ab5dd82
fix: share provider reservations across repos
Pigbibi Jul 11, 2026
f46acf8
fix: enforce codex reservation headroom
Pigbibi Jul 11, 2026
30afc38
fix: reconcile provider spend and codex failures
Pigbibi Jul 11, 2026
f57ff3b
fix: reconcile provider usage deltas
Pigbibi Jul 11, 2026
69eba6c
fix: clean up budget reservations on all exits
Pigbibi Jul 11, 2026
3e6d231
fix: make budget reservations durable and conservative
Pigbibi Jul 11, 2026
4c63c89
fix: persist budget reservations across workers
Pigbibi Jul 11, 2026
bb42d6e
fix: scope ledger rows by repo and billing period
Pigbibi Jul 11, 2026
f7d3885
fix: attribute review budget to authenticated repo
Pigbibi Jul 11, 2026
8e58416
fix: aggregate provider reservations and gate codex explicitly
Pigbibi Jul 11, 2026
bd165a2
fix: reconcile aggregate reservation balances
Pigbibi Jul 11, 2026
2d8090e
fix: expire codex reservations across reset windows
Pigbibi Jul 11, 2026
354845a
fix: fail closed on ledger errors and preserve API primary
Pigbibi Jul 11, 2026
83955a8
fix: fail closed on malformed usage snapshots
Pigbibi Jul 11, 2026
c417901
fix: reconcile monthly API quota accounting
Pigbibi Jul 11, 2026
de84b53
fix: separate repository and aggregate budget limits
Pigbibi Jul 11, 2026
499656f
fix: close review budget rollover leaks
Pigbibi Jul 11, 2026
b024478
fix: close budget period and dispatch lifecycle gaps
Pigbibi Jul 11, 2026
6448fc3
fix: reconcile aggregate settled baselines
Pigbibi Jul 11, 2026
8b6b2c3
fix: share provider reservation scope and preserve errors
Pigbibi Jul 11, 2026
37ac73a
fix: release pre-dispatch job reservations
Pigbibi Jul 11, 2026
06aa96a
fix: release deduped reservations and preserve baselines
Pigbibi Jul 11, 2026
64c426f
fix: preserve safety actions and reconcile batch budgets
Pigbibi Jul 11, 2026
cb46de6
fix: fail closed on quota persistence errors
Pigbibi Jul 11, 2026
855798e
fix: settle async post-dispatch failures safely
Pigbibi Jul 11, 2026
954ce97
fix: settle ambiguous provider failures
Pigbibi Jul 11, 2026
cbc1cad
fix: release non-billable AI reservations
Pigbibi Jul 11, 2026
5f02c2c
fix: preserve budget reservations across failure states
Pigbibi Jul 11, 2026
60f1629
fix: validate review budget ownership
Pigbibi Jul 11, 2026
fc6d78a
fix: reconcile shared scope once per snapshot
Pigbibi Jul 11, 2026
0058005
fix: classify provider dispatch before settlement
Pigbibi Jul 11, 2026
fa4473b
fix: cover guarded completion output budgets
Pigbibi Jul 11, 2026
f6c2aea
fix: make budget ledger transitions idempotent
Pigbibi Jul 11, 2026
0d539b8
fix: scope usage snapshots and persist failure state
Pigbibi Jul 11, 2026
cd2548e
fix: harden budget reset and dispatch accounting
Pigbibi Jul 11, 2026
4429137
fix: settle provider responses and charge verifier execution
Pigbibi Jul 11, 2026
6e80772
fix: persist fail-closed cleanup state
Pigbibi Jul 11, 2026
d90f5ad
fix: fail closed without shared budget ledger
Pigbibi Jul 11, 2026
5769baf
fix: close budget guard failure recovery gaps
Pigbibi Jul 11, 2026
083e1a8
fix: account provider usage for repo budgets
Pigbibi Jul 11, 2026
12b24bc
fix: settle expiring jobs and persist quota failures
Pigbibi Jul 11, 2026
6cf3a79
fix: retain expired jobs pending budget cleanup
Pigbibi Jul 11, 2026
bba62d0
fix: retain ambiguous budget reservations
Pigbibi Jul 11, 2026
3e1e060
fix: retain ambiguous Codex dispatches
Pigbibi Jul 11, 2026
3aa075a
fix: classify provider dispatch outcomes
Pigbibi Jul 11, 2026
8f9f9da
fix: release known local provider failures
Pigbibi Jul 11, 2026
7dc36df
fix: align budget usage windows
Pigbibi Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codex_pr_review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jobs:
CODEX_PR_REVIEW_REUSABLE_CALL: ${{ github.repository != 'QuantStrategyLab/AIAuditBridge' && 'true' || 'false' }}
CODEX_PR_REVIEW_API_FALLBACK_INPUT: ${{ inputs.api_fallback_enabled }}
CODEX_PR_REVIEW_DIRECT_API_PRIMARY_INPUT: ${{ inputs.direct_api_primary_enabled }}
CODEX_PR_REVIEW_API_FALLBACK_DEFAULT: ${{ vars.CODEX_PR_REVIEW_API_FALLBACK_ENABLED || 'true' }}
CODEX_PR_REVIEW_API_FALLBACK_DEFAULT: ${{ vars.CODEX_PR_REVIEW_API_FALLBACK_ENABLED || 'false' }}
CODEX_PR_REVIEW_DIRECT_API_PRIMARY_DEFAULT: ${{ vars.CODEX_PR_REVIEW_DIRECT_API_PRIMARY_ENABLED || 'true' }}
working-directory: source
run: |
Expand Down
21 changes: 21 additions & 0 deletions docs/ai_autonomy_architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,24 @@ trusted review comment 只保存最近固定轮数、固定字节上限且脱敏
2. 再补一份“自治边界表”,把自动 / 人工 / 禁止 三类动作列清楚。
3. 然后补指标持久化和 dashboard 视图。
4. 最后再决定是否扩大 auto-merge 覆盖面。

## 7. AI Budget Guard v1(中央执行前门)

所有 `research`、`optimization`、`review` 和 `auto_fix` 入口在启动前必须生成
`ai_budget_decision.v1` 并取得 `decision=allow`。这是 AIAuditBridge 的中央执行门,
不是各仓库自行复制的 branch rule;确定性监控、kill-switch、暂停和回滚不依赖 AI 额度。

- API-key 预算按 provider、key/project scope、repo、task class 和明确的 billing period
(默认 `UTC` 日历月)记账。未配置月度预算时可用上限为 `0`;内部 hard limit 取用户月度预算
与 provider/project limit 的 `80%` 两者较小值。
- usage/cost 快照缺失、过期或无法解析时 fail closed。每次启动先做原子 reservation,并发
reservation 不能共同突破 hard limit。reservation 的终态由 provider dispatch 证据决定:明确未
dispatch/provider 未接收时才 release;明确已 dispatch、accepted 或 billable 时按 actual 或保守
estimate settle;发送边界崩溃、timeout 或重启后无法证明时持久化为 `pending_uncertain`,继续占用
额度并 fail closed,直到 provider usage、idempotency 或显式 reconciliation 给出证据。不得静默
release,也不得把不确定状态直接永久 settle。
- Codex 同时读取 primary/secondary rate-limit window,使用更紧张的剩余比例;research、
maintenance/review/auto-fix、incident 的门槛分别是 `30%/20%/10%`,并永久保留最后 `10%`。
快照不可用时不启动 research 或 auto-fix。
- Codex 或 API 额度不足的状态是 `deferred_budget`,禁止自动切换 OpenAI/Anthropic、换 key 或
推荐付费低成本模型;只有人工显式批准才允许一次性 fallback。reset 后可安全恢复延后队列。
2 changes: 0 additions & 2 deletions docs/async_service_deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ CODEX_AUDIT_SERVICE_AUDIENCE=quant-codex-audit \
CODEX_AUDIT_SERVICE_MODEL=gpt-5.4 \
CODEX_AUDIT_SERVICE_REASONING_EFFORT=auto \
CODEX_AUDIT_SERVICE_CODEX_ACCOUNT_USAGE=1 \
CODEX_AUDIT_SERVICE_OPENAI_USAGE_WINDOW_DAYS=7 \
CODEX_AUDIT_SERVICE_ANTHROPIC_USAGE_WINDOW_DAYS=7 \
CODEX_AUDIT_SERVICE_JOB_DIR=/var/lib/codex-audit-bridge/jobs \
bash scripts/deploy_codex_audit_service.sh deploy
```
Expand Down
8 changes: 7 additions & 1 deletion scripts/run_codex_pr_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,15 +527,21 @@ def _review_capacity_is_unavailable(exc: ReviewError) -> bool:
"[quota_or_capacity_failure]" in message
or "usage limit" in message
or "daily budget exceeded" in message
or "deferred_budget" in message

This comment was marked as off-topic.

or "codex service job failed [unknown_failure]: codex exec failed" in message
)


def _api_fallback_enabled() -> bool:
return parse_bool(env_value("CODEX_PR_REVIEW_API_FALLBACK_ENABLED", "true"))
# Codex quota exhaustion must become deferred_budget; never consume a
# paid API key unless a human explicitly enables the exception.
return parse_bool(env_value("CODEX_PR_REVIEW_API_FALLBACK_ENABLED", "false"))


def _direct_api_primary_enabled() -> bool:
# Preserve the legacy API-only primary path when no Codex service URL is
# configured; only the secondary fallback after Codex quota failure is off
# by default.
return parse_bool(env_value("CODEX_PR_REVIEW_DIRECT_API_PRIMARY_ENABLED", "true"))


Expand Down
60 changes: 47 additions & 13 deletions service/adapters/codex_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,35 @@

SECRET_ENV_MARKERS = ("TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY", "CREDENTIAL", "API_KEY", "ADMIN_KEY")
CODEX_REASONING_EFFORTS = frozenset({"minimal", "low", "medium", "high", "xhigh"})
_LOCAL_CODEX_FAILURE_MARKERS = (
"authentication",
"bootstrap",
"command not found",
"configuration",
"invalid option",
"invalid sandbox",
"no such file",
"not logged in",
"permission denied",
"unrecognized option",
"unknown option",
"unsupported",
)


@dataclass(frozen=True)
class CodexResult:
success: bool
output: str = ""
error: str = ""
dispatch_started: bool = False
dispatch_uncertain: bool = False


def _codex_dispatch_is_uncertain(detail: str) -> bool:
"""Classify known local CLI failures as definitely not dispatched."""
text = detail.lower()
return not any(marker in text for marker in _LOCAL_CODEX_FAILURE_MARKERS)


def _codex_env() -> dict[str, str]:
Expand Down Expand Up @@ -122,17 +144,21 @@ def execute(

with tempfile.TemporaryDirectory() as tmp:
output_last_message = Path(tmp) / "codex-final-message.md"
try:
command = _codex_command(
output_last_message,
sandbox=sandbox,
model=model,
reasoning_effort=reasoning_effort,
output_schema=output_schema,
cwd=cwd,
images=images,
)
except (RuntimeError, ValueError) as exc:
return CodexResult(success=False, error=f"codex command configuration failed: {exc}")
try:
completed = subprocess.run(
_codex_command(
output_last_message,
sandbox=sandbox,
model=model,
reasoning_effort=reasoning_effort,
output_schema=output_schema,
cwd=cwd,
images=images,
),
command,
input=prompt,
text=True,
stdout=subprocess.PIPE,
Expand All @@ -142,14 +168,22 @@ def execute(
env=_codex_env(),
)
except subprocess.TimeoutExpired as exc:
return CodexResult(success=False, error=f"codex exec timed out after {timeout}s: {exc}")
return CodexResult(
success=False,
error=f"codex exec timed out after {timeout}s: {exc}",
dispatch_uncertain=True,
)
except FileNotFoundError as exc:
return CodexResult(success=False, error=f"codex command not found: {exc}")

if completed.returncode != 0:
detail = (completed.stdout[-4000:] + completed.stderr[-4000:]).strip()
return CodexResult(success=False, error=f"codex exec failed (rc={completed.returncode})" + (f":\n{detail}" if detail else ""))
return CodexResult(
success=False,
error=f"codex exec failed (rc={completed.returncode})" + (f":\n{detail}" if detail else ""),
dispatch_uncertain=_codex_dispatch_is_uncertain(detail),
)

if output_last_message.exists() and output_last_message.read_text(encoding="utf-8").strip():
return CodexResult(success=True, output=output_last_message.read_text(encoding="utf-8"))
return CodexResult(success=True, output=completed.stdout)
return CodexResult(success=True, output=output_last_message.read_text(encoding="utf-8"), dispatch_started=True)
return CodexResult(success=True, output=completed.stdout, dispatch_started=True)
51 changes: 42 additions & 9 deletions service/adapters/llm_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class LlmResult:
success: bool = True
error: str = ""
latency_seconds: float = 0.0
dispatch_started: bool = False
dispatch_uncertain: bool = False


class LlmAdapterError(RuntimeError):
Expand Down Expand Up @@ -105,6 +107,23 @@ def resolve_model(model: str) -> tuple[str, str]:
return PROVIDER_ANTHROPIC, DEFAULT_ANTHROPIC_MODEL


def _dispatch_is_uncertain(error: str) -> bool:
"""Keep capacity only when provider acceptance cannot be disproven."""
text = error.lower()
if "api_key is not configured" in text:
return False
if any(f"http {status}" in text for status in range(400, 500)):
return False
return not any(marker in text for marker in (
"invalid model",
"unsupported model",
"validation",
"malformed",
"missing required",
"not configured",
))


# ── OpenAI ─────────────────────────────────────────────────────────────


Expand Down Expand Up @@ -273,7 +292,13 @@ def complete(
output = _anthropic_completion(resolved_model, system, user, max_tokens=max_tokens, timeout=timeout)
else:
output = _openai_completion(resolved_model, system, user, max_tokens=max_tokens, timeout=timeout)
return LlmResult(provider=provider, model=resolved_model, output=output, latency_seconds=time.time() - started)
return LlmResult(
provider=provider,
model=resolved_model,
output=output,
latency_seconds=time.time() - started,
dispatch_started=True,
)
except LlmAdapterError as exc:
return LlmResult(
provider=provider,
Expand All @@ -282,6 +307,8 @@ def complete(
success=False,
error=str(exc),
latency_seconds=time.time() - started,
dispatch_started=False,
dispatch_uncertain=_dispatch_is_uncertain(str(exc)),
)

def parallel_review(
Expand All @@ -299,18 +326,24 @@ def parallel_review(
"""
import concurrent.futures

results: list[LlmResult] = []
results: list[LlmResult | None] = [None] * len(reviewers)
with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(reviewers), 4)) as pool:
futures = {
pool.submit(self.complete, model=model, system=system, user=user, max_tokens=max_tokens, timeout=timeout): (label, model)
for label, model in reviewers
pool.submit(self.complete, model=model, system=system, user=user, max_tokens=max_tokens, timeout=timeout): (index, label, model)
for index, (label, model) in enumerate(reviewers)
}
for f in concurrent.futures.as_completed(futures):
index, label, model = futures[f]
try:
results.append(f.result())
results[index] = f.result()
except Exception as exc:
label, model = futures[f]
results.append(
LlmResult(provider=label, model=model, output="", success=False, error=str(exc))
results[index] = (
LlmResult(
provider=label,
model=model,
output="",
success=False,
error=str(exc),
)
)
return results
return [result for result in results if result is not None]
Loading
Loading