Skip to content

Commit 6091efd

Browse files
fix: repair a scheme-less WANDB_BASE_URL and stop losing sink failures
Every harbor benchmark run has reported nothing to the self-hosted W&B: `shehab-yasser` listed zero projects, and each run's exported `artifacts/wandb/state.json` showed a run_id minted with `evaluation_ids: []`, `next_step: 0`, `request_log_files: {}` — even for a session that completed 11 evaluations. That fingerprint is exactly what `SidecarWandbSink.__init__` leaves when `wandb.init()` raises: state is written (wandb.py:225) before the run is opened (wandb.py:230), and `deployment.py` catches anything from the constructor and continues without W&B. Reproduced locally against scaleai.wandb.io, $0, deterministic: WANDB_BASE_URL=scaleai.wandb.io pydantic_core.ValidationError: 1 validation error for Settings base_url Input should be a valid URL, relative URL without a base [type=url_parsing, input_value='scaleai.wandb.io'] W&B parses `base_url` as a URL, so the natural way to write a self-hosted host silently costs the run all of its reporting. With `https://` prepended, the same script logs metrics, uploads an artifact and auto-creates the project. Egress was never the problem. Two changes: - `normalize_wandb_base_url()` prepends `https://` to a scheme-less `WANDB_BASE_URL` (and warns), called before both `wandb.init()` sites. Already-qualified values, including plain `http://localhost`, pass through. - The swallowed init failure now also lands in `session/artifacts/wandb/init-error.json`. The existing `logger.warning` goes to the sidecar container's stderr, which no run artifact captures, so a disabled sink was indistinguishable from a healthy run that logged nothing. Observability still never takes the eval path down. Refs #52. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ec708e9 commit 6091efd

4 files changed

Lines changed: 147 additions & 1 deletion

File tree

vero/src/vero/harbor/deployment.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from vero.evaluation.engine import EvaluationEngine
2626
from vero.harbor.backend import HarborBackend, HarborBackendConfig
2727
from vero.models import StrictModel
28+
from vero.runtime.artifacts import ArtifactStore
2829
from vero.sandbox import LocalSandbox
2930
from vero.sidecar.serve import SidecarComponents
3031
from vero.sidecar.session import initialize_harbor_session_manifest
@@ -323,13 +324,28 @@ async def build_harbor_components(config: dict) -> SidecarComponents:
323324
log_traces=parsed.wandb.log_traces,
324325
)
325326
engine.listeners.append(wandb_sink)
326-
except Exception:
327+
except Exception as error:
327328
logger.warning(
328329
"W&B reporting disabled: the sidecar sink could not be "
329330
"initialized; the evaluation sidecar continues without it",
330331
exc_info=True,
331332
)
332333
wandb_sink = None
334+
# The warning above goes to the sidecar container's stderr, which no
335+
# run artifact captures, so a silently disabled sink looks exactly
336+
# like a healthy run that logged nothing. Record why in the session
337+
# artifacts, which are archived into the exported run record.
338+
try:
339+
ArtifactStore(session_dir / "artifacts").write_json(
340+
"wandb/init-error.json",
341+
{
342+
"project": parsed.wandb.project,
343+
"error_type": type(error).__name__,
344+
"error": str(error),
345+
},
346+
)
347+
except OSError:
348+
logger.warning("could not record the W&B init failure", exc_info=True)
333349

334350
usage_path = (
335351
Path(parsed.inference_usage_path)

vero/src/vero/runtime/wandb.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import hashlib
77
import json
88
import logging
9+
import os
910
import tempfile
1011
from pathlib import Path
1112
from typing import Any
@@ -19,6 +20,31 @@
1920
logger = logging.getLogger(__name__)
2021

2122

23+
def normalize_wandb_base_url(environment: dict[str, str] | None = None) -> str | None:
24+
"""Give a scheme-less ``WANDB_BASE_URL`` the ``https://`` it needs.
25+
26+
A self-hosted host is naturally written ``scaleai.wandb.io``, but W&B's
27+
settings model parses ``base_url`` as a URL and rejects that with
28+
``Input should be a valid URL, relative URL without a base``. The error
29+
surfaces out of ``wandb.init()``, which callers treat as "W&B unavailable",
30+
so one missing scheme silently costs a run all of its reporting.
31+
32+
Returns the value in effect, or None when the variable is unset.
33+
"""
34+
environ = os.environ if environment is None else environment
35+
value = (environ.get("WANDB_BASE_URL") or "").strip()
36+
if not value or "://" in value:
37+
return value or None
38+
corrected = f"https://{value}"
39+
environ["WANDB_BASE_URL"] = corrected
40+
logger.warning(
41+
"WANDB_BASE_URL %r has no scheme and would be rejected by W&B; using %r",
42+
value,
43+
corrected,
44+
)
45+
return corrected
46+
47+
2248
def _open_wandb_run(
2349
*,
2450
project: str,
@@ -43,6 +69,7 @@ def _open_wandb_run(
4369
raise RuntimeError(
4470
"W&B reporting requires `pip install scale-vero[wandb]`"
4571
) from error
72+
normalize_wandb_base_url()
4673
wandb_dir.mkdir(parents=True, exist_ok=True)
4774
stable_id = run_id or ("vero-" + hashlib.sha256(session_id.encode()).hexdigest()[:16])
4875
init_kwargs: dict[str, Any] = {
@@ -96,6 +123,7 @@ def __init__(
96123
"W&B reporting requires `pip install scale-vero[wandb]`"
97124
) from error
98125

126+
normalize_wandb_base_url()
99127
wandb_dir = session_dir / "artifacts" / "wandb"
100128
wandb_dir.mkdir(parents=True, exist_ok=True)
101129
self.artifacts = ArtifactStore(session_dir / "artifacts")

vero/tests/test_v05_harbor_deployment.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,76 @@ class Result:
270270
assert (
271271
exported / "requests/requests-00001.jsonl"
272272
).read_text() == '{"scope":"producer"}\n'
273+
274+
275+
@pytest.mark.asyncio
276+
async def test_wandb_init_failure_is_recorded_in_the_session_artifacts(tmp_path):
277+
# A sink that cannot start only logs to the sidecar container's stderr, which
278+
# no run artifact captures, so a disabled sink is indistinguishable from a
279+
# healthy run that logged nothing. Leave the reason behind in the session.
280+
trusted = tmp_path / "trusted"
281+
agent = tmp_path / "agent"
282+
_repo(trusted, "VALUE = 1\n")
283+
_repo(agent, "VALUE = 1\n")
284+
cases = tmp_path / "cases.json"
285+
cases.write_text('[{"id":"task","task_name":"org/task"}]')
286+
evaluation_set = EvaluationSet(name="benchmark")
287+
objective = ObjectiveSpec(
288+
selector=MetricSelector(metric="score"),
289+
direction="maximize",
290+
)
291+
session_dir = tmp_path / "state/session"
292+
config = {
293+
"repo_path": str(trusted),
294+
"agent_repo_path": str(agent),
295+
"session_dir": str(session_dir),
296+
"backends": {
297+
"backend": {
298+
"task_source": "org/benchmark@1.0",
299+
"agent_import_path": "program:Agent",
300+
"cases_path": str(cases),
301+
"harbor_requirement": "harbor==0.1.17",
302+
"uv_executable": sys.executable,
303+
}
304+
},
305+
"access_policies": [],
306+
"budgets": [],
307+
"selection": {
308+
"mode": "auto_best",
309+
"backend_id": "backend",
310+
"evaluation_set": evaluation_set.model_dump(mode="json"),
311+
"objective": objective.model_dump(mode="json"),
312+
"baseline_version": "HEAD",
313+
},
314+
"targets": [
315+
{
316+
"reward_key": "reward",
317+
"backend_id": "backend",
318+
"evaluation_set": evaluation_set.model_dump(mode="json"),
319+
"objective": objective.model_dump(mode="json"),
320+
}
321+
],
322+
"admin_volume": str(tmp_path / "state/admin"),
323+
"wandb": {"project": "vero-tests"},
324+
}
325+
326+
import vero.runtime.wandb as wandb_module
327+
328+
def _explode(**kwargs):
329+
raise ValueError("Input should be a valid URL, relative URL without a base")
330+
331+
original = wandb_module.SidecarWandbSink
332+
wandb_module.SidecarWandbSink = _explode
333+
try:
334+
components = await build_harbor_components(config)
335+
finally:
336+
wandb_module.SidecarWandbSink = original
337+
338+
# The eval path survives: observability never takes the sidecar down.
339+
assert components.telemetry is None
340+
recorded = json.loads(
341+
(session_dir / "artifacts/wandb/init-error.json").read_text()
342+
)
343+
assert recorded["project"] == "vero-tests"
344+
assert recorded["error_type"] == "ValueError"
345+
assert "valid URL" in recorded["error"]

vero/tests/test_v05_wandb.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import os
45
from datetime import UTC, datetime, timedelta
56
from pathlib import Path
67

@@ -402,3 +403,31 @@ def test_sidecar_wandb_telemetry_is_best_effort(tmp_path: Path):
402403
poller.poll_once() # must not raise
403404
assert client.run.logged == []
404405
assert client.run.artifacts == []
406+
407+
408+
def test_scheme_less_wandb_base_url_is_repaired_before_init(tmp_path: Path, monkeypatch):
409+
"""A self-hosted host written without a scheme must not silently kill reporting.
410+
411+
W&B parses `base_url` as a URL, so `WANDB_BASE_URL=scaleai.wandb.io` raises
412+
out of `wandb.init()` and the sidecar disables W&B for the whole run.
413+
"""
414+
from vero.runtime.wandb import normalize_wandb_base_url
415+
416+
monkeypatch.setenv("WANDB_BASE_URL", "scaleai.wandb.io")
417+
assert normalize_wandb_base_url() == "https://scaleai.wandb.io"
418+
assert os.environ["WANDB_BASE_URL"] == "https://scaleai.wandb.io"
419+
420+
# Already-qualified values, including plain http, are left exactly as given.
421+
monkeypatch.setenv("WANDB_BASE_URL", "http://localhost:8080")
422+
assert normalize_wandb_base_url() == "http://localhost:8080"
423+
assert os.environ["WANDB_BASE_URL"] == "http://localhost:8080"
424+
425+
monkeypatch.delenv("WANDB_BASE_URL", raising=False)
426+
assert normalize_wandb_base_url() is None
427+
428+
# And the repair happens before a sink opens its run.
429+
monkeypatch.setenv("WANDB_BASE_URL", "scaleai.wandb.io")
430+
SidecarWandbSink(
431+
project="v", session_id="s", session_dir=tmp_path / "session", client=FakeWandb()
432+
)
433+
assert os.environ["WANDB_BASE_URL"] == "https://scaleai.wandb.io"

0 commit comments

Comments
 (0)