2026-10-20 TUE UTC 12:42 PM TPE 8:42 PM Auto-updates hourly 中文 EN
LIVE WIRE
← Back to the site changelog Site Changelog

Original commit record

Configure model priority and quality profiles (#30) Set Gemini 3.6 Flash as the default, align the fallback and usage display order, and tune per-model quality profiles.

SHA
026dd73cfc901ea6928753e3048cccb8a90ca354
Commit date
Author
nnneeeooo-nnneeeooo
Committer
web-flow

Changed files (7)

  • README.md modified · +3 / −3
  • pipeline/build.py modified · +20 / −2
  • pipeline/model_config.py added · +18 / −0
  • pipeline/providers.py modified · +43 / −44
  • tests/test_providers_payloads.py modified · +26 / −3
  • tests/test_usage.py modified · +16 / −0
  • tests/test_write_offline.py modified · +4 / −4

Full diff

@@ -94,10 +94,10 @@ python -m http.server -d site 8000\n | `ANTHROPIC_API_KEY` | — | Anthropic 撰稿(選用) |\n | `GEMINI_API_KEY` | — | Google Gemini 撰稿(選用,免費層) |\n | `NVIDIA_API_KEY` | — | NVIDIA NIM 撰稿(選用,免費額度) |\n-| `AVWIRE_PROVIDER_ORDER` | `anthropic,gemini,nvidia` | 撰稿優先鏈,主力在前;token 可用 `平台:模型` 指定模型,同平台可重複出現(例:`nvidia:nvidia/nemotron-3-ultra-550b-a55b,nvidia:nvidia/nemotron-3-super-120b-a12b,gemini`)。主力模型驗證失敗自動重試一次;帳號/額度失效會跳過同平台所有備援模型 |\n+| `AVWIRE_PROVIDER_ORDER` | `gemini:gemini-3.6-flash,gemini:gemini-3.5-flash,nvidia:z-ai/glm-5.2,nvidia:deepseek-ai/deepseek-v4-pro,nvidia:nvidia/nemotron-3-ultra-550b-a55b,nvidia:qwen/qwen3.5-397b-a17b,nvidia:nvidia/nemotron-3-super-120b-a12b,nvidia:mistralai/mistral-medium-3.5-128b` | 撰稿優先鏈,主力在前;主力模型驗證失敗自動重試一次,之後依序備援;帳號/額度失效會跳過同平台所有備援模型 |\n | `AVWIRE_MODEL` | `claude-opus-5` | Anthropic 模型 |\n-| `AVWIRE_GEMINI_MODEL` | `gemini-2.5-flash` | Gemini 模型 |\n-| `AVWIRE_NVIDIA_MODEL` | `deepseek-ai/deepseek-v3.1` | NVIDIA NIM 模型 |\n+| `AVWIRE_GEMINI_MODEL` | `gemini-3.6-flash` | Gemini 模型 |\n+| `AVWIRE_NVIDIA_MODEL` | `z-ai/glm-5.2` | NVIDIA NIM 模型 |\n | `NEWS_MAX_AGE_HOURS` | `120` | 新聞新鮮度窗口(小時),驗證範圍 24–336,非法值回退預設並警告 |\n | `AEROAPI_KEY` | — | FlightAware AeroAPI(選用統計) |\n | `AVWIRE_BASE_PATH` | `/avwire` | 站台子路徑(Pages 專案站台) |\n@@ -41,6 +41,7 @@\n     parse_iso,\n     story_category,\n )\n+from model_config import DEFAULT_PROVIDER_ORDER\n \n # Asia/Taipei via zoneinfo where tz data exists (Linux CI); the zone has had a\n # fixed +08:00 offset with no DST since 1980, so a static fallback is exact.\n@@ -1417,12 +1418,26 @@ def _usage_price_for(label: str, prices: dict):\n     return None\n \n \n+def _configured_model_priority() -> dict:\n+    configured = (os.environ.get("AVWIRE_PROVIDER_ORDER")\n+                  or DEFAULT_PROVIDER_ORDER)\n+    return {\n+        token.strip(): index\n+        for index, token in enumerate(configured.split(","))\n+        if token.strip()\n+    }\n+\n+\n def usage_rows(ledger: dict, prices: dict):\n     """Per-model rows + totals with theoretical USD value at list prices."""\n     rows = []\n     totals = {"calls": 0, "in": 0, "out": 0, "usd": 0.0,\n               "unknown": 0, "all_priced": True}\n-    for label, m in sorted((ledger.get("models") or {}).items()):\n+    priority = _configured_model_priority()\n+    models = (ledger.get("models") or {}).items()\n+    for label, m in sorted(\n+            models,\n+            key=lambda item: (priority.get(item[0], len(priority)), item[0])):\n         if not isinstance(m, dict):\n             continue\n         calls = int(m.get("calls") or 0)\n@@ -1645,7 +1660,10 @@ def recent_run_view(ledger: dict, now=None) -> dict:\n                    if row["duration_count"] else None)\n         row["average_duration"] = _duration_text(average)\n         model_rows.append(row)\n-    model_rows.sort(key=lambda row: (-row["attempts"], row["label"]))\n+    priority = _configured_model_priority()\n+    model_rows.sort(\n+        key=lambda row: (\n+            priority.get(row["label"], len(priority)), row["label"]))\n \n     totals = [run["durations"]["total"] for run in runs\n               if run["durations"]["total"] is not None]\n@@ -0,0 +1,18 @@\n+"""Shared AVWIRE model priority configuration.\n+\n+The first entry is the default writer. The same order drives provider\n+fallback and model ordering on the private usage dashboard.\n+"""\n+\n+MODEL_ORDER = (\n+    "gemini:gemini-3.6-flash",\n+    "gemini:gemini-3.5-flash",\n+    "nvidia:z-ai/glm-5.2",\n+    "nvidia:deepseek-ai/deepseek-v4-pro",\n+    "nvidia:nvidia/nemotron-3-ultra-550b-a55b",\n+    "nvidia:qwen/qwen3.5-397b-a17b",\n+    "nvidia:nvidia/nemotron-3-super-120b-a12b",\n+    "nvidia:mistralai/mistral-medium-3.5-128b",\n+)\n+\n+DEFAULT_PROVIDER_ORDER = ",".join(MODEL_ORDER)\n@@ -20,8 +20,8 @@\n         raises ProviderError      failure of this one call (truncation,\n                bad JSON, transient HTTP) -> try the next provider\n \n-The write stage tries providers in AVWIRE_PROVIDER_ORDER (default\n-"anthropic,gemini,nvidia") and falls through to the next on failure.\n+The write stage tries providers in AVWIRE_PROVIDER_ORDER (defaulting to the\n+configured MODEL_ORDER below) and falls through to the next on failure.\n An order token may pin a model with "name:model", and the same platform\n may appear multiple times with different models, e.g.:\n \n@@ -40,13 +40,14 @@\n import time\n \n import requests\n+from model_config import DEFAULT_PROVIDER_ORDER, MODEL_ORDER\n \n try:\n     import anthropic\n except ImportError:  # pragma: no cover - anthropic is in requirements.txt\n     anthropic = None\n \n-DEFAULT_ORDER = "anthropic,gemini,nvidia"\n+DEFAULT_ORDER = DEFAULT_PROVIDER_ORDER\n # 550B-class NIM models regularly need >120s on the free tier.\n HTTP_TIMEOUT = (10, 180)  # (connect, read) seconds\n \n@@ -59,29 +60,47 @@\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 = "nvidia/nemotron-3-ultra-550b-a55b"\n-GEMINI_DEFAULT_MODEL = "gemini-2.5-flash"\n+NVIDIA_DEFAULT_MODEL = "z-ai/glm-5.2"\n+GEMINI_DEFAULT_MODEL = "gemini-3.6-flash"\n \n MODEL_PROFILES: dict = {\n-    "nvidia/nemotron-3-ultra-550b-a55b": {\n-        # Medium thinking via the official Nemotron chat-template kwargs.\n-        "payload": {"temperature": 1.0, "top_p": 0.95,\n-                    "chat_template_kwargs": {"enable_thinking": True,\n-                                             "medium_effort": True}},\n-        "repair": {"chat_template_kwargs": {"enable_thinking": False}},\n+    # Gemini thinking is high for fact-sensitive bilingual drafting. Format\n+    # repair remains minimal because that call may only fix JSON syntax.\n+    # Temperature is intentionally omitted: Gemini 3.x uses its tuned default.\n+    "gemini-3.6-flash": {\n+        "generationConfig": {"maxOutputTokens": 16384,\n+                             "thinkingConfig": {"thinkingLevel": "high"}},\n+        "repairGenerationConfig": {\n+            "maxOutputTokens": 8192,\n+            "thinkingConfig": {"thinkingLevel": "minimal"}},\n     },\n-    "nvidia/nemotron-3-super-120b-a12b": {\n-        # Low effort via the official top-level field: the default full\n-        # reasoning ate the whole 8192-token budget in live runs.\n-        "payload": {"temperature": 1.0, "top_p": 0.95,\n-                    "reasoning_effort": "low"},\n-        "repair": {"reasoning_effort": "none"},\n+    "gemini-3.5-flash": {\n+        "generationConfig": {"maxOutputTokens": 16384,\n+                             "thinkingConfig": {"thinkingLevel": "high"}},\n+        "repairGenerationConfig": {\n+            "maxOutputTokens": 8192,\n+            "thinkingConfig": {"thinkingLevel": "minimal"}},\n+    },\n+    "z-ai/glm-5.2": {\n+        # The public NIM schema for GLM exposes no reasoning control.\n+        # Low temperature improves factual consistency and JSON stability.\n+        "reasoningMode": "provider_default",\n+        "payload": {"temperature": 0.1},\n+        "repair": {},\n     },\n     "deepseek-ai/deepseek-v4-pro": {\n+        # High reasoning is retained for evidence-heavy verification work.\n         "payload": {"temperature": 1.0, "top_p": 0.95,\n                     "reasoning_effort": "high"},\n         "repair": {"reasoning_effort": "none"},\n     },\n+    "nvidia/nemotron-3-ultra-550b-a55b": {\n+        # Medium thinking balances verification quality with usable output.\n+        "payload": {"temperature": 1.0, "top_p": 0.95,\n+                    "chat_template_kwargs": {"enable_thinking": True,\n+                                             "medium_effort": True}},\n+        "repair": {"chat_template_kwargs": {"enable_thinking": False}},\n+    },\n     "qwen/qwen3.5-397b-a17b": {\n         # NIM exposes thinking on/off only for Qwen; thinking-mode sampling\n         # per the model card.\n@@ -90,38 +109,18 @@\n                     "chat_template_kwargs": {"enable_thinking": True}},\n         "repair": {"chat_template_kwargs": {"enable_thinking": False}},\n     },\n-    "z-ai/glm-5.2": {\n-        # The public NIM schema for GLM exposes no reasoning control;\n-        # provider-default reasoning is used deliberately. The marker below\n-        # is internal documentation and is never sent on the wire.\n-        "reasoningMode": "provider_default",\n-        "payload": {"temperature": 0.1},\n-        "repair": {},\n+    "nvidia/nemotron-3-super-120b-a12b": {\n+        # Low effort avoids consuming the full output budget before the\n+        # bilingual JSON answer, a failure observed with full reasoning.\n+        "payload": {"temperature": 1.0, "top_p": 0.95,\n+                    "reasoning_effort": "low"},\n+        "repair": {"reasoning_effort": "none"},\n     },\n     "mistralai/mistral-medium-3.5-128b": {\n-        # Last fallback: rarely called, quality over latency -> high.\n-        # NIM offers none|high only for this model.\n+        # NIM offers none|high only; keep high for the final fallback.\n         "payload": {"temperature": 0.7, "reasoning_effort": "high"},\n         "repair": {"reasoning_effort": "none"},\n     },\n-    # No temperature on Gemini 3.x: Google's docs recommend keeping the\n-    # tuned defaults (the field is deprecated on newer 3.6-flash builds).\n-    # 16384 out: fulltext-enriched groups produce long bilingual bodies,\n-    # and thinking tokens count against maxOutputTokens on Gemini.\n-    "gemini-3.6-flash": {\n-        "generationConfig": {"maxOutputTokens": 16384,\n-                             "thinkingConfig": {"thinkingLevel": "medium"}},\n-        "repairGenerationConfig": {\n-            "maxOutputTokens": 8192,\n-            "thinkingConfig": {"thinkingLevel": "minimal"}},\n-    },\n-    "gemini-3.5-flash": {\n-        "generationConfig": {"maxOutputTokens": 16384,\n-                             "thinkingConfig": {"thinkingLevel": "medium"}},\n-        "repairGenerationConfig": {\n-            "maxOutputTokens": 8192,\n-            "thinkingConfig": {"thinkingLevel": "minimal"}},\n-    },\n }\n \n REPAIR_SYSTEM_PROMPT = (\n@@ -130,6 +130,28 @@ def fake_post(url, json=None, timeout=None, headers=None):\n     print("test_nvidia_model_profiles: done")\n \n \n+def test_model_priority_defaults():\n+    expected = (\n+        "gemini:gemini-3.6-flash",\n+        "gemini:gemini-3.5-flash",\n+        "nvidia:z-ai/glm-5.2",\n+        "nvidia:deepseek-ai/deepseek-v4-pro",\n+        "nvidia:nvidia/nemotron-3-ultra-550b-a55b",\n+        "nvidia:qwen/qwen3.5-397b-a17b",\n+        "nvidia:nvidia/nemotron-3-super-120b-a12b",\n+        "nvidia:mistralai/mistral-medium-3.5-128b",\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+    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+    print("test_model_priority_defaults: done")\n+\n+\n def test_nvidia_reasoning_trace_never_parsed():\n     """reasoning_content is ignored; empty final content is a failure."""\n     def fake_post(url, json=None, timeout=None, headers=None):\n@@ -182,7 +204,7 @@ def fake_post(url, json=None, timeout=None, headers=None):\n \n \n def test_gemini_model_profiles():\n-    """Gemini 3.6/3.5 send thinkingLevel medium; unknown models send none."""\n+    """Gemini 3.6/3.5 send thinkingLevel high; unknown models send none."""\n     for model in ("gemini-3.6-flash", "gemini-3.5-flash"):\n         captured = []\n \n@@ -195,8 +217,8 @@ def fake_post(url, json=None, timeout=None, headers=None):\n                            lambda: provider.draft("sys", "user", SCHEMA))\n         check(result == {"ok": 1}, f"{model}: draft parsed")\n         config = captured[0]["generationConfig"]\n-        check(config.get("thinkingConfig") == {"thinkingLevel": "medium"},\n-              f"{model}: thinkingLevel medium")\n+        check(config.get("thinkingConfig") == {"thinkingLevel": "high"},\n+              f"{model}: thinkingLevel high")\n         check("temperature" not in config\n               and config.get("maxOutputTokens") == 16384,\n               f"{model}: no temperature on Gemini 3.x (tuned defaults)")\n@@ -271,6 +293,7 @@ def fake_post(url, json=None, timeout=None, headers=None):\n \n def main():\n     tests = [\n+        test_model_priority_defaults,\n         test_nvidia_model_profiles,\n         test_nvidia_reasoning_trace_never_parsed,\n         test_nvidia_format_repair,\n@@ -249,6 +249,17 @@ def json(self):\n prices = json.loads((REPO / "config" / "model_prices.json")\n                     .read_text(encoding="utf-8"))\n rows, totals = build.usage_rows(usage.load_ledger(), prices)\n+priority_ledger = {\n+    "models": {\n+        label: {"calls": 1, "inputTokens": 0, "outputTokens": 0,\n+                "unknownCalls": 0}\n+        for label in reversed(providers.MODEL_ORDER)\n+    }\n+}\n+priority_rows, _ = build.usage_rows(priority_ledger, prices)\n+check("usage dashboard follows the configured model priority",\n+      [row["label"] for row in priority_rows]\n+      == list(providers.MODEL_ORDER))\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",\n@@ -400,6 +411,11 @@ def json(self):\n       [row["group_id"] for row in view_with_old["runs"]]\n       == ["boeing-777-9-test-hours", "nan"]\n       and view_with_old["runs"][1]["durations"]["total"] is None)\n+check("recent model statistics follow configured model priority",\n+      [row["label"] for row in view_with_old["models"]]\n+      == ["gemini:gemini-3.6-flash",\n+          "nvidia:nvidia/nemotron-3-ultra-550b-a55b",\n+          "nvidia:nvidia/nemotron-3-super-120b-a12b"])\n check("page is noindex and shows totals",\n       "noindex" in page and "API 用量" in page and "$0" in page)\n check("page shows exact Codex GPT-5 task totals and cost semantics",\n@@ -203,7 +203,7 @@ def test_publish_flow():\n                            "zh": {}, "en": {}, "sources": [{"name": "x", "url": "y"}]}]}\n     hour_path.write_text(json.dumps(dummy), encoding="utf-8")\n \n-    os.environ["ANTHROPIC_API_KEY"] = "test-key-not-real"\n+    os.environ["GEMINI_API_KEY"] = "test-key-not-real"\n \n     def fake_draft(client, group):\n         if group["id"] == "g-20260726-0503-1":\n@@ -240,7 +240,7 @@ def fake_draft(client, group):\n     check(isinstance(biz["zh"]["body"], list) and isinstance(biz["en"]["body"], list),\n           "zh/en bodies are paragraph lists")\n     check(biz["image"] is None, "biz article image is null")\n-    check(biz.get("writer", "").startswith("anthropic:"),\n+    check(biz.get("writer", "").startswith("gemini:"),\n           "writer provenance recorded")\n     check(len(biz.get("facts", [])) == 2, "verified facts stored on article")\n     check(biz.get("riskFlags") == [], "publish requires empty risk flags")\n@@ -256,7 +256,7 @@ def fake_draft(client, group):\n     entry = review[0]\n     check(entry["id"] == "g-20260726-0503-1", f"queued group id: {entry['id']}")\n     check(entry["approve"] is False, "queued entries await human approval")\n-    check(entry["writer"].startswith("anthropic:"), "queue records the writer")\n+    check(entry["writer"].startswith("gemini:"), "queue records the writer")\n     check(len(entry["facts"]) == 2, "verified facts stored with the entry")\n     check(entry["riskFlags"] == ["accident_or_serious_incident", "investigation"],\n           f"risk flags on queue entry, got {entry['riskFlags']}")\n@@ -389,7 +389,7 @@ def test_group_cap_and_unique_ids():\n         json.dumps({"generatedUtc": "2026-07-26T05:03Z", "groups": groups}),\n         encoding="utf-8")\n \n-    os.environ["ANTHROPIC_API_KEY"] = "test-key-not-real"\n+    os.environ["GEMINI_API_KEY"] = "test-key-not-real"\n     # Same en title every time; the quote must match THESE groups' titles.\n     cap_draft = json.loads(json.dumps(DRAFT_BIZ))\n     cap_draft["facts"] = [

You found a secret theme!

8-bit mode is unlocked. Refresh to add it to your theme selector.