A3-R5H Gemini free live probe

#107
Files changed (2) hide show
  1. app.py +7 -0
  2. core/a3_gemini_live_probe.py +227 -0
app.py CHANGED
@@ -48,6 +48,13 @@ try:
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.
 
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
+ # A3-R5H: default-off Gemini Free-Tier startup probe; never blocks the UI.
52
+ try:
53
+ from core.a3_gemini_live_probe import run_startup_probe_if_enabled as run_gemini_startup_probe_if_enabled
54
+ run_gemini_startup_probe_if_enabled()
55
+ except Exception as _a3_gemini_probe_exc:
56
+ logging.error("[A3_R5H_GEMINI_PROBE] bootstrap_failed error_class=%s", _a3_gemini_probe_exc.__class__.__name__)
57
+
58
  WORKSPACE_DIR = APP_CONFIG.get("workspace_dir", ".workspace")
59
  os.makedirs(WORKSPACE_DIR, exist_ok=True)
60
  # The display-only preview cache must exist from the first Streamlit script run.
core/a3_gemini_live_probe.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """A3-R5H isolated Gemini Free-Tier 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 come from the versioned A3 benchmark fixture.
7
+ A separate explicit Free-Tier confirmation flag is required before any request.
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.gemini_rest_adapter import GeminiRestAdapter
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
+ gemini_cfg = dict(adapters.get("gemini") or {})
38
+ if not gemini_cfg:
39
+ raise RuntimeError("gemini 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() != "gemini":
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 gemini mechanical and reasoning profiles are required")
53
+
54
+ pool = dict(data.get("free_provider_pool") or {})
55
+ if str(pool.get("price_contract") or "") != "input_and_output_zero_required":
56
+ raise RuntimeError("free-provider zero-price contract is missing")
57
+ if "gemini" not in {str(item).strip().lower() for item in list(pool.get("providers") or [])}:
58
+ raise RuntimeError("gemini is not authorized in the benchmark free-provider pool")
59
+ if bool(pool.get("paid_fallback_allowed")):
60
+ raise RuntimeError("paid fallback must remain disabled for the free-provider pool")
61
+
62
+ base_url_env = str(gemini_cfg.get("base_url_env") or "").strip()
63
+ base_url = str(os.getenv(base_url_env) or gemini_cfg.get("base_url") or "").strip()
64
+ api_key_env = str(gemini_cfg.get("api_key_env") or "").strip()
65
+ if not api_key_env:
66
+ raise RuntimeError("gemini api_key_env is not configured")
67
+ api_key = str(os.getenv(api_key_env) or "").strip()
68
+ if not api_key:
69
+ raise RuntimeError("configured Gemini API key environment variable is unavailable")
70
+ if not base_url:
71
+ raise RuntimeError("configured Gemini base URL is unavailable")
72
+
73
+ return {
74
+ "benchmark_version": str(data.get("benchmark_version") or ""),
75
+ "adapter": gemini_cfg,
76
+ "base_url": base_url,
77
+ "api_key": api_key,
78
+ "profiles": selected,
79
+ }
80
+
81
+
82
+ def _zero_cost_guard() -> None:
83
+ # Human-confirmed project status is intentionally separate from the API key.
84
+ # Default false means an accidental deployment/restart cannot send a request.
85
+ if not _truthy(os.getenv("A3_R5H_GEMINI_FREE_TIER_CONFIRMED")):
86
+ raise RuntimeError("Gemini Free-Tier project status has not been explicitly confirmed")
87
+
88
+
89
+ def _event_dict(event: Any) -> dict[str, Any]:
90
+ if hasattr(event, "to_dict") and callable(event.to_dict):
91
+ return dict(event.to_dict())
92
+ if isinstance(event, Mapping):
93
+ return dict(event)
94
+ return {}
95
+
96
+
97
+ def _safe_summary(case_id: str, profile_id: str, event: Mapping[str, Any], *, content_ok: bool) -> dict[str, Any]:
98
+ return {
99
+ "case_id": case_id,
100
+ "profile_id": profile_id,
101
+ "status": str(event.get("status") or ""),
102
+ "provider": str(event.get("provider") or ""),
103
+ "model": str(event.get("provider_model_version") or event.get("model") or ""),
104
+ "streaming": bool(event.get("streaming")),
105
+ "thinking_mode": str(event.get("thinking_mode") or ""),
106
+ "finish_reason": str(event.get("finish_reason") or ""),
107
+ "http_status": event.get("http_status"),
108
+ "latency_ms": int(event.get("latency_ms") or 0),
109
+ "prompt_input_tokens": int(event.get("prompt_input_tokens") or 0),
110
+ "output_tokens": int(event.get("output_tokens") or 0),
111
+ "reasoning_or_thought_tokens": int(event.get("reasoning_or_thought_tokens") or 0),
112
+ "total_tokens": int(event.get("total_tokens") or 0),
113
+ "content_ok": bool(content_ok),
114
+ }
115
+
116
+
117
+ def _adapter_for(cfg: Mapping[str, Any], profile_id: str, profile: Mapping[str, Any], case_id: str, events: list[dict[str, Any]]) -> GeminiRestAdapter:
118
+ def sink(event: Any) -> None:
119
+ events.append(_event_dict(event))
120
+
121
+ return GeminiRestAdapter(
122
+ api_key=str(cfg["api_key"]),
123
+ model_name=str(profile.get("model") or "").strip(),
124
+ base_url=str(cfg["base_url"]),
125
+ timeout_seconds=float(dict(cfg["adapter"]).get("timeout_seconds") or 120),
126
+ usage_sink=sink,
127
+ usage_context={
128
+ "phase_id": "a3_r5h_gemini_free_live_probe",
129
+ "case_id": case_id,
130
+ "benchmark_version": str(cfg["benchmark_version"]),
131
+ "profile_id": profile_id,
132
+ "attempt_index": 1,
133
+ },
134
+ )
135
+
136
+
137
+ def run_gemini_live_probe() -> dict[str, Any]:
138
+ """Run three tiny Free-Tier calls: ordinary, structured JSON, and stream."""
139
+ _zero_cost_guard()
140
+ cfg = _load_probe_config()
141
+ results: list[dict[str, Any]] = []
142
+
143
+ mechanical_id, mechanical = cfg["profiles"][False]
144
+ reasoning_id, reasoning = cfg["profiles"][True]
145
+
146
+ # 1) Ordinary generateContent.
147
+ events: list[dict[str, Any]] = []
148
+ adapter = _adapter_for(cfg, mechanical_id, mechanical, "ordinary", events)
149
+ text = adapter.call(
150
+ "Connectivity capability probe.",
151
+ "Reply with exactly OK.",
152
+ max_tokens=16,
153
+ temp=0.0,
154
+ model_name=adapter.model_name,
155
+ thinking=False,
156
+ allow_fallback=False,
157
+ )
158
+ results.append(_safe_summary("ordinary", mechanical_id, events[-1] if events else {}, content_ok=str(text or "").strip() == "OK"))
159
+
160
+ # 2) Structured JSON generateContent.
161
+ events = []
162
+ adapter = _adapter_for(cfg, mechanical_id, mechanical, "structured_json", events)
163
+ text = adapter.call(
164
+ "Structured-output capability probe.",
165
+ 'Return a JSON object with exactly one field: {"ok": true}.',
166
+ max_tokens=32,
167
+ temp=0.0,
168
+ model_name=adapter.model_name,
169
+ thinking=False,
170
+ allow_fallback=False,
171
+ response_format={"type": "json_object"},
172
+ )
173
+ try:
174
+ parsed = json.loads(str(text or ""))
175
+ json_ok = isinstance(parsed, Mapping) and parsed.get("ok") is True and len(parsed) == 1
176
+ except Exception:
177
+ json_ok = False
178
+ results.append(_safe_summary("structured_json", mechanical_id, events[-1] if events else {}, content_ok=json_ok))
179
+
180
+ # 3) streamGenerateContent using the free reasoning profile.
181
+ events = []
182
+ adapter = _adapter_for(cfg, reasoning_id, reasoning, "stream", events)
183
+ chunks = list(adapter.stream(
184
+ "Streaming capability probe.",
185
+ "Reply with exactly OK.",
186
+ max_tokens=32,
187
+ temp=0.0,
188
+ model_name=adapter.model_name,
189
+ thinking=True,
190
+ reasoning_effort=str(reasoning.get("reasoning_effort") or "low"),
191
+ allow_fallback=False,
192
+ ))
193
+ results.append(_safe_summary("stream", reasoning_id, events[-1] if events else {}, content_ok="".join(chunks).strip() == "OK"))
194
+
195
+ passed = all(
196
+ row.get("status") == "success"
197
+ and row.get("http_status") == 200
198
+ and row.get("content_ok")
199
+ and int(row.get("total_tokens") or 0) > 0
200
+ for row in results
201
+ )
202
+ return {"probe": "A3-R5H", "status": "pass" if passed else "fail", "results": results}
203
+
204
+
205
+ def run_startup_probe_if_enabled() -> None:
206
+ """Run at most once per process, only when both explicit safety flags allow it."""
207
+ global _RAN
208
+ if not _truthy(os.getenv("A3_R5H_GEMINI_PROBE_ENABLED")):
209
+ return
210
+ with _LOCK:
211
+ if _RAN:
212
+ return
213
+ _RAN = True
214
+
215
+ try:
216
+ summary = run_gemini_live_probe()
217
+ _LOG.warning("[A3_R5H_GEMINI_PROBE] %s", json.dumps(summary, ensure_ascii=False, sort_keys=True))
218
+ except Exception as exc:
219
+ # Never log provider response bodies, prompts, headers, API keys, or raw exception strings.
220
+ _LOG.error(
221
+ "[A3_R5H_GEMINI_PROBE] %s",
222
+ json.dumps({
223
+ "probe": "A3-R5H",
224
+ "status": "error",
225
+ "error_class": exc.__class__.__name__,
226
+ }, sort_keys=True),
227
+ )