From 4cbcf9197924417475037aef7820b8cc77933ed1 Mon Sep 17 00:00:00 2001 From: hxddh Date: Wed, 22 Jul 2026 01:00:21 +0800 Subject: [PATCH] Use tiered low-cost AI Gateway fallbacks --- .github/workflows/cloud-agent.yml | 3 ++- CHANGELOG.md | 1 + README.md | 2 +- docs/cloud-agent.md | 5 +++-- docs/subscription-mode.md | 5 +++-- scripts/cloud_agent_runner.py | 15 ++++++++++++--- tests/test_cloud_agent_runner.py | 11 +++++++++++ 7 files changed, 33 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cloud-agent.yml b/.github/workflows/cloud-agent.yml index 6b5012d..25cc48c 100644 --- a/.github/workflows/cloud-agent.yml +++ b/.github/workflows/cloud-agent.yml @@ -61,7 +61,8 @@ jobs: INCLUDE_MAINTENANCE_CONTEXT: ${{ vars.INCLUDE_MAINTENANCE_CONTEXT || 'false' }} INCLUDE_RUNBOOK_CONTEXT: ${{ vars.INCLUDE_RUNBOOK_CONTEXT || 'false' }} DRY_RUN_ON_BUDGET_EXCEEDED: ${{ vars.DRY_RUN_ON_BUDGET_EXCEEDED || 'true' }} - AI_GATEWAY_FALLBACK_MODELS: ${{ vars.AI_GATEWAY_FALLBACK_MODELS || 'google/gemini-2.5-flash-lite' }} + AI_GATEWAY_FALLBACK_MODELS: ${{ vars.AI_GATEWAY_FALLBACK_MODELS || 'openai/gpt-5-nano' }} + AI_GATEWAY_SCREEN_FALLBACK_MODELS: ${{ vars.AI_GATEWAY_SCREEN_FALLBACK_MODELS || 'google/gemini-2.5-flash-lite' }} AI_GATEWAY_MAX_OUTPUT_TOKENS: ${{ vars.AI_GATEWAY_MAX_OUTPUT_TOKENS || '32768' }} # Tuned in-repo (repo Actions variables held stale lower values and # silently clamped collection breadth; see docs/release-v0.19.0.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff6bd1..69680f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - AI Gateway requests rely on the existing JSON-only prompt and strict parser instead of `response_format=json_object`, which DeepSeek V4 rejects through the Gateway. - Added an explicit 32,768-token output ceiling and low-cost fallback recovery for HTTP 200 responses whose model content is truncated or otherwise invalid JSON. - Normalize pure Markdown JSON fences before strict parsing so a valid fenced response does not spend a fallback call. +- Split fallback by workload: Flash Lite backs up Nano screening, while Nano backs up GPT-OSS synthesis after Flash Lite failed the weekly bilingual-content quality gate. - A zero AI Gateway call budget now completes as a real dry run instead of failing the empty-update guard. ## v0.19.5 - 2026-07-18 diff --git a/README.md b/README.md index 54886bb..d2a0842 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Daily notes are append-oriented. Use them to record high-signal items without fo For fully Cloud Agent-driven operation, use `automation/daily.md`. -For true 24/7 cloud execution, `.github/workflows/cloud-agent.yml` uses GitHub Models by default with the built-in GitHub Actions `GITHUB_TOKEN`. The recommended cost-controlled mode uses Vercel AI Gateway directly with GPT-5 Nano for screening, GPT-OSS 120B for research and final synthesis, Gemini 2.5 Flash Lite only as a transient-failure fallback, and free public sources only; see `docs/cloud-agent.md`. +For true 24/7 cloud execution, `.github/workflows/cloud-agent.yml` uses GitHub Models by default with the built-in GitHub Actions `GITHUB_TOKEN`. The recommended cost-controlled mode uses Vercel AI Gateway directly with GPT-5 Nano for screening, GPT-OSS 120B for research and final synthesis, Nano again as the synthesis fallback, Gemini 2.5 Flash Lite only as the screening fallback, and free public sources only; see `docs/cloud-agent.md`. Cloud mode uses source lanes, source-cache novelty tracking, scoring, source health, lane health, and structured telemetry. Daily, weekly, and monthly reports use nested bilingual pairs: each substantive item is a label bullet with `中文:` first and `English:` second as sub-bullets; short metadata fields stay on one line as `中文值(English value)`; URLs and product names are written once. At least 60% of substantive English lines must have a real Chinese counterpart. diff --git a/docs/cloud-agent.md b/docs/cloud-agent.md index dad71d6..0384dfb 100644 --- a/docs/cloud-agent.md +++ b/docs/cloud-agent.md @@ -53,7 +53,8 @@ REDDIT_SUBREDDITS=LocalLLaMA,MachineLearning,ClaudeAI,GithubCopilot,Cursor,ChatG MAX_AI_GATEWAY_CALLS_PER_TASK= MAX_PROMPT_CHARS=120000 DRY_RUN_ON_BUDGET_EXCEEDED=true -AI_GATEWAY_FALLBACK_MODELS=google/gemini-2.5-flash-lite +AI_GATEWAY_FALLBACK_MODELS=openai/gpt-5-nano +AI_GATEWAY_SCREEN_FALLBACK_MODELS=google/gemini-2.5-flash-lite AI_GATEWAY_MAX_OUTPUT_TOKENS=32768 MAX_RELEASE_REPOS=20 MAX_RELEASES_PER_REPO=3 @@ -82,7 +83,7 @@ Model routing stays bounded but discovery-oriented: - `promote-candidates`: GPT-OSS 120B automatically promotes at most 3 high-quality candidates from `research-log.md`. - `weekly` and `monthly`: GPT-5 Nano screens public signals, then GPT-OSS 120B performs final synthesis (default `MAX_AI_GATEWAY_CALLS_PER_TASK=2`). -Gemini 2.5 Flash Lite is not part of the normal route. It is tried once only after a 408/429/5xx response, a transport timeout, an empty response, or malformed/truncated JSON. The 32,768-token ceiling is an output cap rather than prepaid usage; tokens are billed only when generated. +Fallback is tiered by workload and is not part of the normal route. A failed Nano screening call tries Gemini 2.5 Flash Lite; a failed GPT-OSS synthesis call tries Nano, which is more reliable for the long bilingual schema. Fallback is limited to a 408/429/5xx response, transport timeout, empty response, or malformed/truncated JSON. The 32,768-token ceiling is an output cap rather than prepaid usage; tokens are billed only when generated. This keeps paid search calls at zero. Model usage is bounded by the fixed task route, `MAX_PUBLIC_SOURCE_ITEMS`, `MAX_AI_GATEWAY_CALLS_PER_TASK`, and `MAX_PROMPT_CHARS`. diff --git a/docs/subscription-mode.md b/docs/subscription-mode.md index c7d7306..16e1ec5 100644 --- a/docs/subscription-mode.md +++ b/docs/subscription-mode.md @@ -93,7 +93,8 @@ MAX_PUBLIC_SOURCE_ITEMS= PUBLIC_SOURCE_COLLECTION=true MAX_PROMPT_CHARS=120000 DRY_RUN_ON_BUDGET_EXCEEDED=true -AI_GATEWAY_FALLBACK_MODELS=google/gemini-2.5-flash-lite +AI_GATEWAY_FALLBACK_MODELS=openai/gpt-5-nano +AI_GATEWAY_SCREEN_FALLBACK_MODELS=google/gemini-2.5-flash-lite AI_GATEWAY_MAX_OUTPUT_TOKENS=32768 MAX_RELEASE_REPOS=12 MAX_RELEASES_PER_REPO=2 @@ -108,4 +109,4 @@ Behavior: - Promote-candidates runs automatically promote at most 3 high-quality candidates. - Daily runs use GPT-5 Nano for screening and GPT-OSS 120B for final updates. - Weekly/monthly runs use GPT-5 Nano for screening, then GPT-OSS 120B for final synthesis (default `MAX_AI_GATEWAY_CALLS_PER_TASK=2`). -- Gemini 2.5 Flash Lite is tried only for transient HTTP/transport failures or invalid/truncated JSON, keeping fallback output at the same low price tier as the primary route. +- Fallback remains workload-aware and low-cost: Flash Lite backs up Nano screening, while Nano backs up GPT-OSS long-form synthesis after transient HTTP/transport failures or invalid/truncated JSON. diff --git a/scripts/cloud_agent_runner.py b/scripts/cloud_agent_runner.py index e689fd3..a64d668 100644 --- a/scripts/cloud_agent_runner.py +++ b/scripts/cloud_agent_runner.py @@ -5445,9 +5445,16 @@ def ai_gateway_headers() -> dict[str, str]: def ai_gateway_fallback_models(model: str) -> list[str]: - fallback = split_env_list( - "AI_GATEWAY_FALLBACK_MODELS", [DEFAULT_AI_GATEWAY_FALLBACK_MODEL] - ) + cheap = os.environ.get("CHEAP_SCREEN_MODEL", DEFAULT_CHEAP_SCREEN_MODEL) + if model == cheap: + fallback = split_env_list( + "AI_GATEWAY_SCREEN_FALLBACK_MODELS", [DEFAULT_AI_GATEWAY_FALLBACK_MODEL] + ) + else: + # Reuse Nano for synthesis recovery: it is cheaper than Mini, retains a + # long output window, and avoids asking Flash Lite to write the long + # bilingual reports that it failed the production quality gate on. + fallback = split_env_list("AI_GATEWAY_FALLBACK_MODELS", [DEFAULT_CHEAP_SCREEN_MODEL]) models = [model] for item in fallback: if item not in models: @@ -5495,6 +5502,8 @@ def call_ai_gateway_model(prompt: str, model: str) -> dict[str, Any]: audit_model(candidate_model) if candidate_model != model: RUN_AUDIT["fallbacks"].append(f"{model}->{candidate_model}") + reason = last_error.split(":", 1)[0] if last_error else "transient failure" + print(f"AI Gateway fallback {model}->{candidate_model}: {reason}") if attempt > 0: # Brief backoff before retrying a transient failure. time.sleep(min(8, 2 ** attempt)) diff --git a/tests/test_cloud_agent_runner.py b/tests/test_cloud_agent_runner.py index db8b5de..7c2b4b1 100644 --- a/tests/test_cloud_agent_runner.py +++ b/tests/test_cloud_agent_runner.py @@ -55,6 +55,17 @@ def test_ai_gateway_default_model_route_uses_nano_and_gpt_oss(self) -> None: ["openai/gpt-oss-120b"], ) + def test_ai_gateway_default_fallbacks_are_tiered_by_workload(self) -> None: + with mock.patch.dict(os.environ, {}, clear=True): + self.assertEqual( + cloud_agent_runner.ai_gateway_fallback_models("openai/gpt-5-nano"), + ["openai/gpt-5-nano", "google/gemini-2.5-flash-lite"], + ) + self.assertEqual( + cloud_agent_runner.ai_gateway_fallback_models("openai/gpt-oss-120b"), + ["openai/gpt-oss-120b", "openai/gpt-5-nano"], + ) + def test_auto_tasks_include_candidate_promotion_on_sunday(self) -> None: tasks = cloud_agent_runner.auto_tasks(cloud_agent_runner.parse_date("2026-07-05")) self.assertIn("daily", tasks)