A3-R5D GLM live capability probe harness

#105
Files changed (2) hide show
  1. app.py +7 -0
  2. core/a3_glm_live_probe.py +189 -0
app.py CHANGED
@@ -41,6 +41,13 @@ st.set_page_config(
41
  initial_sidebar_state="collapsed",
42
  )
43
 
 
 
 
 
 
 
 
44
  WORKSPACE_DIR = APP_CONFIG.get("workspace_dir", ".workspace")
45
  os.makedirs(WORKSPACE_DIR, exist_ok=True)
46
  # The display-only preview cache must exist from the first Streamlit script run.
 
41
  initial_sidebar_state="collapsed",
42
  )
43
 
44
+ # A3-R5D: default-off, benchmark-only startup probe; never blocks the UI.
45
+ try:
46
+ from core.a3_glm_live_probe import run_startup_probe_if_enabled
47
+ run_startup_probe_if_enabled()
48
+ except Exception as _a3_probe_exc:
49
+ logging.error("[A3_R5D_GLM_PROBE] bootstrap_failed error_class=%s", _a3_probe_exc.__class__.__name__)
50
+
51
  WORKSPACE_DIR = APP_CONFIG.get("workspace_dir", ".workspace")
52
  os.makedirs(WORKSPACE_DIR, exist_ok=True)
53
  # The display-only preview cache must exist from the first Streamlit script run.
core/a3_glm_live_probe.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """A3-R5D isolated GLM live-capability startup probe.
3
+
4
+ Default-off, benchmark-only diagnostic. It never changes product routing,
5
+ quota/payment state, manuscript generation, or UI. Provider/model/base URL and
6
+ API-key environment names are resolved from the versioned A3 benchmark fixture
7
+ rather than hard-coded in this module.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import os
14
+ from pathlib import Path
15
+ import threading
16
+ from typing import Any, Mapping
17
+
18
+ from core.provider_adapters.glm_rest_adapter import ZhipuGLMRestAdapter
19
+
20
+ _LOG = logging.getLogger(__name__)
21
+ _LOCK = threading.Lock()
22
+ _RAN = False
23
+
24
+
25
+ def _truthy(value: Any) -> bool:
26
+ return str(value or "").strip().lower() in {"1", "true", "yes", "on", "enabled"}
27
+
28
+
29
+ def _fixture_path() -> Path:
30
+ return Path(__file__).resolve().parents[1] / "benchmarks" / "fixtures" / "a3_benchmark_cases.json"
31
+
32
+
33
+ def _load_probe_config() -> dict[str, Any]:
34
+ data = json.loads(_fixture_path().read_text(encoding="utf-8"))
35
+ profiles = data.get("profiles") or {}
36
+ adapters = data.get("provider_adapters") or {}
37
+ zhipu_cfg = dict(adapters.get("zhipu") or {})
38
+ if not zhipu_cfg:
39
+ raise RuntimeError("zhipu benchmark adapter configuration is missing")
40
+
41
+ selected: dict[bool, tuple[str, Mapping[str, Any]]] = {}
42
+ for profile_id, raw in profiles.items():
43
+ profile = dict(raw or {})
44
+ if str(profile.get("provider") or "").strip().lower() != "zhipu":
45
+ continue
46
+ if str(profile.get("monetary_cost_class") or "").strip().lower() != "free":
47
+ continue
48
+ thinking = bool(profile.get("thinking"))
49
+ selected.setdefault(thinking, (str(profile_id), profile))
50
+
51
+ if False not in selected or True not in selected:
52
+ raise RuntimeError("both free zhipu thinking profiles are required for the live probe")
53
+
54
+ base_url_env = str(zhipu_cfg.get("base_url_env") or "").strip()
55
+ base_url = str(os.getenv(base_url_env) or zhipu_cfg.get("base_url") or "").strip()
56
+ api_key_env = str(zhipu_cfg.get("api_key_env") or "").strip()
57
+ if not api_key_env:
58
+ raise RuntimeError("zhipu api_key_env is not configured")
59
+ api_key = str(os.getenv(api_key_env) or "").strip()
60
+ if not api_key:
61
+ raise RuntimeError("configured zhipu API key environment variable is unavailable")
62
+ if not base_url:
63
+ raise RuntimeError("configured zhipu base URL is unavailable")
64
+
65
+ return {
66
+ "benchmark_version": str(data.get("benchmark_version") or ""),
67
+ "adapter": zhipu_cfg,
68
+ "base_url": base_url,
69
+ "api_key": api_key,
70
+ "profiles": selected,
71
+ }
72
+
73
+
74
+ def _event_dict(event: Any) -> dict[str, Any]:
75
+ if hasattr(event, "to_dict") and callable(event.to_dict):
76
+ return dict(event.to_dict())
77
+ if isinstance(event, Mapping):
78
+ return dict(event)
79
+ return {}
80
+
81
+
82
+ def _safe_summary(profile_id: str, event: Mapping[str, Any], *, content_ok: bool) -> dict[str, Any]:
83
+ return {
84
+ "profile_id": profile_id,
85
+ "status": str(event.get("status") or ""),
86
+ "provider": str(event.get("provider") or ""),
87
+ "model": str(event.get("provider_model_version") or event.get("model") or ""),
88
+ "streaming": bool(event.get("streaming")),
89
+ "thinking_mode": str(event.get("thinking_mode") or ""),
90
+ "finish_reason": str(event.get("finish_reason") or ""),
91
+ "http_status": event.get("http_status"),
92
+ "latency_ms": int(event.get("latency_ms") or 0),
93
+ "prompt_input_tokens": int(event.get("prompt_input_tokens") or 0),
94
+ "cache_hit_tokens": int(event.get("cache_hit_tokens") or 0),
95
+ "cache_miss_tokens": int(event.get("cache_miss_tokens") or 0),
96
+ "output_tokens": int(event.get("output_tokens") or 0),
97
+ "reasoning_or_thought_tokens": int(event.get("reasoning_or_thought_tokens") or 0),
98
+ "total_tokens": int(event.get("total_tokens") or 0),
99
+ "content_ok": bool(content_ok),
100
+ }
101
+
102
+
103
+ def run_glm_live_probe() -> dict[str, Any]:
104
+ """Run two tiny real calls: static/no-thinking and stream/thinking."""
105
+ cfg = _load_probe_config()
106
+ results: list[dict[str, Any]] = []
107
+
108
+ for thinking, streaming in ((False, False), (True, True)):
109
+ profile_id, profile = cfg["profiles"][thinking]
110
+ events: list[dict[str, Any]] = []
111
+
112
+ def sink(event: Any) -> None:
113
+ events.append(_event_dict(event))
114
+
115
+ adapter = ZhipuGLMRestAdapter(
116
+ api_key=cfg["api_key"],
117
+ model_name=str(profile.get("model") or "").strip(),
118
+ base_url=cfg["base_url"],
119
+ timeout_seconds=float(cfg["adapter"].get("timeout_seconds") or 120),
120
+ usage_sink=sink,
121
+ usage_context={
122
+ "phase_id": "a3_r5d_live_probe",
123
+ "case_id": "stream_thinking" if streaming else "static_no_thinking",
124
+ "benchmark_version": cfg["benchmark_version"],
125
+ "profile_id": profile_id,
126
+ "attempt_index": 1,
127
+ },
128
+ )
129
+ if not adapter.model_name:
130
+ raise RuntimeError("zhipu benchmark profile model is missing")
131
+
132
+ system_prompt = "Connectivity capability probe."
133
+ user_prompt = "Reply with exactly OK."
134
+ if streaming:
135
+ chunks = list(adapter.stream(
136
+ system_prompt,
137
+ user_prompt,
138
+ max_tokens=128,
139
+ model_name=adapter.model_name,
140
+ thinking=True,
141
+ temp=0.0,
142
+ allow_fallback=False,
143
+ ))
144
+ content_ok = bool("".join(chunks).strip())
145
+ else:
146
+ text = adapter.call(
147
+ system_prompt,
148
+ user_prompt,
149
+ max_tokens=16,
150
+ temp=0.0,
151
+ model_name=adapter.model_name,
152
+ thinking=False,
153
+ allow_fallback=False,
154
+ )
155
+ content_ok = bool(str(text or "").strip())
156
+
157
+ event = events[-1] if events else {}
158
+ results.append(_safe_summary(profile_id, event, content_ok=content_ok))
159
+
160
+ return {
161
+ "probe": "A3-R5D",
162
+ "status": "pass" if all(row.get("status") == "success" and row.get("content_ok") for row in results) else "fail",
163
+ "results": results,
164
+ }
165
+
166
+
167
+ def run_startup_probe_if_enabled() -> None:
168
+ """Run at most once per process, only when the explicit probe flag is enabled."""
169
+ global _RAN
170
+ if not _truthy(os.getenv("A3_R5D_GLM_PROBE_ENABLED")):
171
+ return
172
+ with _LOCK:
173
+ if _RAN:
174
+ return
175
+ _RAN = True
176
+
177
+ try:
178
+ summary = run_glm_live_probe()
179
+ _LOG.warning("[A3_R5D_GLM_PROBE] %s", json.dumps(summary, ensure_ascii=False, sort_keys=True))
180
+ except Exception as exc:
181
+ # Do not log provider response text, prompts, headers, API keys, or raw exception strings.
182
+ _LOG.error(
183
+ "[A3_R5D_GLM_PROBE] %s",
184
+ json.dumps({
185
+ "probe": "A3-R5D",
186
+ "status": "error",
187
+ "error_class": exc.__class__.__name__,
188
+ }, sort_keys=True),
189
+ )