{
  "sha": "a6015884b14a3358abd60985969e36b5321d3450",
  "message": "移除自動撰稿鏈中的失效模型路由",
  "author": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "",
    "email": ""
  },
  "committer": {
    "login": "web-flow",
    "name": "",
    "email": ""
  },
  "created_at": "2026-08-30T22:45:14Z",
  "files": [
    {
      "filename": ".github/workflows/briefing.yml",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 1,
      "changes": 2
    },
    {
      "filename": ".github/workflows/hourly.yml",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 1,
      "changes": 2
    },
    {
      "filename": "pipeline/model_config.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 15,
      "deletions": 5,
      "changes": 20
    },
    {
      "filename": "pipeline/providers.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 1,
      "changes": 2
    },
    {
      "filename": "tests/test_manual_draft.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 4,
      "deletions": 2,
      "changes": 6
    },
    {
      "filename": "tests/test_providers_payloads.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 25,
      "deletions": 12,
      "changes": 37
    },
    {
      "filename": "tests/test_usage.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 4,
      "deletions": 1,
      "changes": 5
    }
  ],
  "diff": "@@ -81,7 +81,7 @@ jobs:\\n           OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}\\n           WECHAT_API_KEY: ${{ secrets.DeepSeekV4Flash_API }}\\n           AVWIRE_WECHAT_MODEL: ${{ vars.AVWIRE_WECHAT_MODEL }}\\n-          AVWIRE_PROVIDER_ORDER: opencode:claude-sonnet-4-6,opencode:gpt-5.5,gemini:gemini-3.6-flash,opencode:gemini-3.1-pro,opencode:qwen3.6-plus,nvidia:z-ai/glm-5.2,opencode:glm-5.1,opencode:kimi-k2.6,gemini:gemini-3.5-flash,nvidia:deepseek-ai/deepseek-v4-pro,nvidia:nvidia/nemotron-3-ultra-550b-a55b,wechat:Deepseek-v4-flash,openrouter:nvidia/nemotron-3-ultra-550b-a55b:free,nvidia:qwen/qwen3.5-397b-a17b,nvidia:nvidia/nemotron-3-super-120b-a12b,openrouter:nvidia/nemotron-3-super-120b-a12b:free,nvidia:mistralai/mistral-medium-3.5-128b,openrouter:google/gemma-4-31b-it:free,openrouter:inclusionai/ling-3.0-flash:free,openrouter:google/gemma-4-26b-a4b-it:free,openrouter:nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free,openrouter:openai/gpt-oss-20b:free,openrouter:nvidia/nemotron-3-nano-30b-a3b:free,openrouter:nvidia/nemotron-nano-12b-v2-vl:free,openrouter:nvidia/nemotron-nano-9b-v2:free,openrouter:poolside/laguna-s-2.1:free,openrouter:cohere/north-mini-code:free,openrouter:poolside/laguna-xs-2.1:free\\n+          AVWIRE_PROVIDER_ORDER: gemini:gemini-3.6-flash,gemini:gemini-3.5-flash,nvidia:nvidia/nemotron-3-ultra-550b-a55b,wechat:Deepseek-v4-flash,openrouter:nvidia/nemotron-3-ultra-550b-a55b:free,nvidia:nvidia/nemotron-3-super-120b-a12b\\n           BRIEFING_LLM_INTRO: ${{ vars.BRIEFING_LLM_INTRO }}\\n           BRIEFING_MAX_ITEMS: ${{ vars.BRIEFING_MAX_ITEMS }}\\n           BRIEFING_MAX_ITEMS_PER_SECTION: ${{ vars.BRIEFING_MAX_ITEMS_PER_SECTION }}\\n@@ -66,7 +66,7 @@ jobs:\\n           AVWIRE_GEMINI_MODEL: ${{ vars.AVWIRE_GEMINI_MODEL }}\\n           AVWIRE_NVIDIA_MODEL: ${{ vars.AVWIRE_NVIDIA_MODEL }}\\n           AVWIRE_WECHAT_MODEL: ${{ vars.AVWIRE_WECHAT_MODEL }}\\n-          AVWIRE_PROVIDER_ORDER: opencode:claude-sonnet-4-6,opencode:gpt-5.5,gemini:gemini-3.6-flash,opencode:gemini-3.1-pro,opencode:qwen3.6-plus,nvidia:z-ai/glm-5.2,opencode:glm-5.1,opencode:kimi-k2.6,gemini:gemini-3.5-flash,nvidia:deepseek-ai/deepseek-v4-pro,nvidia:nvidia/nemotron-3-ultra-550b-a55b,wechat:Deepseek-v4-flash,openrouter:nvidia/nemotron-3-ultra-550b-a55b:free,nvidia:qwen/qwen3.5-397b-a17b,nvidia:nvidia/nemotron-3-super-120b-a12b,openrouter:nvidia/nemotron-3-super-120b-a12b:free,nvidia:mistralai/mistral-medium-3.5-128b,openrouter:google/gemma-4-31b-it:free,openrouter:inclusionai/ling-3.0-flash:free,openrouter:google/gemma-4-26b-a4b-it:free,openrouter:nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free,openrouter:openai/gpt-oss-20b:free,openrouter:nvidia/nemotron-3-nano-30b-a3b:free,openrouter:nvidia/nemotron-nano-12b-v2-vl:free,openrouter:nvidia/nemotron-nano-9b-v2:free,openrouter:poolside/laguna-s-2.1:free,openrouter:cohere/north-mini-code:free,openrouter:poolside/laguna-xs-2.1:free\\n+          AVWIRE_PROVIDER_ORDER: gemini:gemini-3.6-flash,gemini:gemini-3.5-flash,nvidia:nvidia/nemotron-3-ultra-550b-a55b,wechat:Deepseek-v4-flash,openrouter:nvidia/nemotron-3-ultra-550b-a55b:free,nvidia:nvidia/nemotron-3-super-120b-a12b\\n           # freshness window (hours); code default 120, validated 24-336\\n           NEWS_MAX_AGE_HOURS: ${{ vars.NEWS_MAX_AGE_HOURS }}\\n           # private usage-dashboard URL token (page skipped when unset)\\n@@ -44,10 +44,8 @@\\n     \"openrouter:poolside/laguna-xs-2.1:free\",\\n )\\n \\n-# Rank by model capability first, then by provider for equivalent models.\\n-# Primary provider routes remain ahead of OpenRouter's free fallbacks. The\\n-# WeChat Flash route stays below the primary models because its gateway\\n-# disables hidden reasoning for predictable output.\\n+# Complete model catalog used by the manual drafting workbench and historical\\n+# usage dashboard.\\n MODEL_ORDER = (\\n     OPENCODE_MODEL_ORDER[0],\\n     OPENCODE_MODEL_ORDER[1],\\n@@ -67,7 +65,19 @@\\n     *OPENROUTER_MODEL_ORDER[2:],\\n )\\n \\n-DEFAULT_PROVIDER_ORDER = \",\".join(MODEL_ORDER)\\n+# Automated runs use only routes that completed at least one valid article in\\n+# recent production telemetry. Retired/unavailable routes stay selectable in\\n+# the manual workbench but no longer add doomed HTTP 410 calls to every story.\\n+AUTOMATIC_MODEL_ORDER = (\\n+    DIRECT_MODEL_ORDER[0],\\n+    DIRECT_MODEL_ORDER[1],\\n+    DIRECT_MODEL_ORDER[4],\\n+    WECHAT_MODEL_ORDER[0],\\n+    OPENROUTER_MODEL_ORDER[0],\\n+    DIRECT_MODEL_ORDER[6],\\n+)\\n+\\n+DEFAULT_PROVIDER_ORDER = \",\".join(AUTOMATIC_MODEL_ORDER)\\n \\n MODEL_DISPLAY_NAMES = {\\n     \"wechat:Deepseek-v4-flash\":\\n@@ -70,7 +70,7 @@\\n #           (JSON syntax only - content failures NEVER take this path)\\n # Only fields the respective endpoint documents are sent; models without a\\n # profile fall back to conservative generic settings.\\n-NVIDIA_DEFAULT_MODEL = \"z-ai/glm-5.2\"\\n+NVIDIA_DEFAULT_MODEL = \"nvidia/nemotron-3-ultra-550b-a55b\"\\n GEMINI_DEFAULT_MODEL = \"gemini-3.6-flash\"\\n OPENROUTER_DEFAULT_MODEL = \"nvidia/nemotron-3-ultra-550b-a55b:free\"\\n OPENCODE_DEFAULT_MODEL = \"claude-sonnet-4-6\"\\n@@ -16,6 +16,7 @@\\n import manual_draft  # noqa: E402\\n import write  # noqa: E402\\n from model_config import (  # noqa: E402\\n+    AUTOMATIC_MODEL_ORDER,\\n     MODEL_DISPLAY_NAMES,\\n     MODEL_ORDER,\\n     OPENCODE_MODEL_ORDER,\\n@@ -817,9 +818,10 @@ def test_static_security_contract():\\n           and \"encrypted envelopes only\" in workflow\\n           and \"data/manual-jobs/outbox 2>/dev/null || true\" in workflow,\\n           \"Actions uses Secrets, checks ciphertext and tolerates empty queues\")\\n-    configured_order = f\"AVWIRE_PROVIDER_ORDER: {','.join(MODEL_ORDER)}\"\\n+    configured_order = (\\n+        f\"AVWIRE_PROVIDER_ORDER: {','.join(AUTOMATIC_MODEL_ORDER)}\")\\n     check(configured_order in hourly and configured_order in briefing,\\n-          \"automatic workflows use the complete shared fallback order\")\\n+          \"automatic workflows use the proven production fallback order\")\\n     check(\"secrets.OPENROUTER_API_KEY\" in hourly\\n           and \"secrets.OPENROUTER_API_KEY\" in briefing\\n           and \"sk-or-v1-\" in hourly and \"sk-or-v1-\" in briefing,\\n@@ -25,6 +25,10 @@\\n os.environ.setdefault(\"OPENCODE_API_KEY\", \"oc_sk_test-not-real\")\\n \\n import providers  # noqa: E402\\n+from model_config import (  # noqa: E402\\n+    AUTOMATIC_MODEL_ORDER,\\n+    OPENROUTER_MODEL_ORDER,\\n+)\\n \\n _pass = 0\\n _fail = 0\\n@@ -166,12 +170,22 @@ def test_model_priority_defaults():\\n     )\\n     check(providers.MODEL_ORDER == expected,\\n           \"model priority order matches the configured product order\")\\n-    check(providers.DEFAULT_ORDER == \",\".join(expected),\\n-          \"fallback default derives from model priority order\")\\n+    active = (\\n+        \"gemini:gemini-3.6-flash\",\\n+        \"gemini:gemini-3.5-flash\",\\n+        \"nvidia:nvidia/nemotron-3-ultra-550b-a55b\",\\n+        \"wechat:Deepseek-v4-flash\",\\n+        \"openrouter:nvidia/nemotron-3-ultra-550b-a55b:free\",\\n+        \"nvidia:nvidia/nemotron-3-super-120b-a12b\",\\n+    )\\n+    check(AUTOMATIC_MODEL_ORDER == active\\n+          and providers.DEFAULT_ORDER == \",\".join(active),\\n+          \"automatic fallback default contains only proven live routes\")\\n     check(providers.GEMINI_DEFAULT_MODEL == \"gemini-3.6-flash\",\\n           \"Gemini 3.6 Flash is the default model\")\\n-    check(providers.NVIDIA_DEFAULT_MODEL == \"z-ai/glm-5.2\",\\n-          \"GLM 5.2 is the highest-priority NVIDIA default\")\\n+    check(providers.NVIDIA_DEFAULT_MODEL\\n+          == \"nvidia/nemotron-3-ultra-550b-a55b\",\\n+          \"Nemotron Ultra is the highest-priority NVIDIA default\")\\n     check(providers.OPENROUTER_DEFAULT_MODEL\\n           == \"nvidia/nemotron-3-ultra-550b-a55b:free\",\\n           \"Nemotron Ultra is the highest-priority OpenRouter default\")\\n@@ -186,7 +200,7 @@ def test_model_priority_defaults():\\n     ) and expected[14:16] == (\\n         \"nvidia:nvidia/nemotron-3-super-120b-a12b\",\\n         \"openrouter:nvidia/nemotron-3-super-120b-a12b:free\",\\n-    ), \"primary routes stay before WeChat Flash and free fallbacks\")\\n+    ), \"complete catalog preserves owner model order\")\\n     print(\"test_model_priority_defaults: done\")\\n \\n \\n@@ -289,10 +303,9 @@ def fake_repair(url, json=None, timeout=None, headers=None):\\n def test_openrouter_models_and_payloads():\\n     models = [\\n         token.split(\":\", 1)[1]\\n-        for token in providers.MODEL_ORDER\\n-        if token.startswith(\"openrouter:\")\\n+        for token in OPENROUTER_MODEL_ORDER\\n     ]\\n-    check(len(models) == 13, \"all 13 OpenRouter models are configured\")\\n+    check(len(models) == 13, \"all 13 OpenRouter profiles remain supported\")\\n     for model in models:\\n         captured = []\\n \\n@@ -380,9 +393,9 @@ def fake_repair(url, json=None, timeout=None, headers=None):\\n             else:\\n                 os.environ[key] = value\\n     check(automatic == [\\n-        token for token in providers.MODEL_ORDER\\n+        token for token in AUTOMATIC_MODEL_ORDER\\n         if token.startswith(\"openrouter:\")\\n-    ], \"automatic writer exposes all OpenRouter fallbacks in owner order\")\\n+    ], \"automatic writer exposes only proven OpenRouter fallbacks\")\\n \\n     try:\\n         provider._final_text(FakeResponse({\"error\": {\"code\": 401}}))\\n@@ -513,9 +526,9 @@ def fake_post(url, json=None, timeout=None, headers=None):\\n             else:\\n                 os.environ[key] = value\\n     check(automatic == [\\n-        token for token in providers.MODEL_ORDER\\n+        token for token in AUTOMATIC_MODEL_ORDER\\n         if token.startswith(\"opencode:\")\\n-    ], \"automatic writer exposes all OpenCode fallbacks in owner order\")\\n+    ], \"automatic writer excludes unavailable OpenCode routes\")\\n     print(\"test_opencode_protocols_and_payloads: done\")\\n \\n \\n@@ -28,6 +28,7 @@\\n import providers  # noqa: E402\\n import usage  # noqa: E402\\n from common import now_utc  # noqa: E402\\n+from model_config import AUTOMATIC_MODEL_ORDER  # noqa: E402\\n \\n CHECKS = 0\\n FAILED = 0\\n@@ -300,9 +301,11 @@ def json(self):\\n     }\\n }\\n priority_rows, _ = build.usage_rows(priority_ledger, prices)\\n+expected_priority = list(AUTOMATIC_MODEL_ORDER) + sorted(\\n+    set(providers.MODEL_ORDER) - set(AUTOMATIC_MODEL_ORDER))\\n check(\"usage dashboard follows the configured model priority\",\\n       [row[\"label\"] for row in priority_rows]\\n-      == list(providers.MODEL_ORDER))\\n+      == expected_priority)\\n ultra = next(r for r in rows if \"ultra\" in r[\"label\"])\\n expect_usd = (ultra[\"in\"] / 1e6 * 0.423) + (ultra[\"out\"] / 1e6 * 2.61)\\n check(\"theoretical USD uses the reference price table\","
}
