From f67d105269ba7f625f1ab95642beb51de3f9d677 Mon Sep 17 00:00:00 2001 From: Dmitry Khanukov Date: Thu, 5 Nov 2026 01:21:34 +0000 Subject: [PATCH 1/4] feat: add minimum-reasoning Sonnet 5.5 campaign profile Signed-off-by: Dmitry Khanukov --- tests/test_selected_six_http.py | 14 +- tests/test_three_flow_november.py | 260 ++++++++++++++++++++++++++++++ tools/three_flow_admission.py | 16 +- tools/three_flow_campaign.py | 4 +- tools/three_flow_http.py | 28 +++- tools/three_flow_live.py | 1 + tools/three_flow_profiles.py | 33 +++- tools/three_flow_selected.py | 36 +++-- 8 files changed, 366 insertions(+), 26 deletions(-) create mode 100644 tests/test_three_flow_november.py diff --git a/tests/test_selected_six_http.py b/tests/test_selected_six_http.py index 8267853..bd352f9 100644 --- a/tests/test_selected_six_http.py +++ b/tests/test_selected_six_http.py @@ -165,7 +165,15 @@ def http(self, slot): return mock -def fixture(tmp_path, provider, model, fault=None, *, river_pacer=None): +def fixture( + tmp_path, + provider, + model, + fault=None, + *, + river_pacer=None, + roster_profile=SELECTED_SIX_PROFILE, +): from tools.three_flow_admission import VARIABLES from tools.three_flow_budget import CampaignBudget from tools.three_flow_campaign import assignments @@ -186,12 +194,12 @@ def fixture(tmp_path, provider, model, fault=None, *, river_pacer=None): campaign_id="selected-test", cap=None, limit_policy=policy, - roster_profile=SELECTED_SIX_PROFILE, + roster_profile=roster_profile, create=True, ) slot = next( s - for s in assignments("selected-test", SELECTED_SIX_PROFILE) + for s in assignments("selected-test", roster_profile) if s["model"] == model and s["flow"] == "commerce" ) p = { diff --git a/tests/test_three_flow_november.py b/tests/test_three_flow_november.py new file mode 100644 index 0000000..996f8a5 --- /dev/null +++ b/tests/test_three_flow_november.py @@ -0,0 +1,260 @@ +"""November finite roster contract; all responses are synthetic SDK wires.""" + +import json + +import pytest + +from tests.test_selected_six_http import fixture +from tools.three_flow_profiles import registry, selected_roster + +PROFILE = "november-model-expansion-v1" +MODEL = "claude-sonnet-5-5" + + +def test_november_finite_roster_and_mapping(): + from tools.three_flow_campaign import CONFIGURATIONS, assignments, preflight + + assert selected_roster(PROFILE) == (("anthropic", MODEL),) + rows = registry(roster_profile=PROFILE, mode_profile="off-or-minimum-v1") + assert rows[0]["request_settings"]["thinking"] == {"type": "between_tools"} + assert rows[0]["request_settings"]["tool_choice"] == { + "type": "auto", + "disable_parallel_tool_use": True, + } + assert rows[0]["mapping"] == "three-flow-anthropic-november-stateless-v1" + assert rows[0]["request_settings"]["no_native_continuation"] is True + slots = assignments("november", PROFILE) + assert len(slots) == 44 + assert {(s["flow"], s["scenario"]) for s in slots} == set(CONFIGURATIONS) + assert preflight(PROFILE)["global_cap_usd"] is None + + +def assert_projection(call, request): + from operatebench.agents import anthropic_messages as aa + + assert call["system"] == aa.request_instructions(request) + assert call["messages"] == [ + { + "role": "user", + "content": json.dumps( + dict(request.prompt), + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ), + } + ] + assert call["tools"] == aa.anthropic_outcome_tools() + + +def test_sonnet_sdk_transcript_and_accounting(tmp_path, monkeypatch): + import httpx + + from tools.three_flow_mock import SDKMockTransport + from tools.three_flow_runtime import replay_trial, run_trial + + original = SDKMockTransport.handle + + def thinking_response(self, request): + response = original(self, request) + body = json.loads(response.content) + body["content"].insert( + 0, + { + "type": "thinking", + "thinking": "synthetic progress", + "signature": "offline-signature", + }, + ) + return httpx.Response(200, json=body) + + monkeypatch.setattr(SDKMockTransport, "handle", thinking_response) + budget, t, trial, network = fixture( + tmp_path, "anthropic", MODEL, roster_profile=PROFILE + ) + requests = [] + original_send = t.send + + def capture(request): + requests.append(request) + return original_send(request) + + monkeypatch.setattr(t, "send", capture) + try: + record = run_trial(trial, t, output=tmp_path / "trial") + assert record["evaluation"]["reliable"] + assert t.settings["native_continuation"] is False + assert t.settings["no_native_continuation"] is True + calls = network.transports[0].calls + assert len(calls) > 1 + for i, call in enumerate(calls): + assert call["thinking"] == {"type": "between_tools"} + assert call["output_config"] == {"effort": "low"} + assert call["tool_choice"] == { + "type": "auto", + "disable_parallel_tool_use": True, + } + assert not {"temperature", "top_p", "top_k"} & call.keys() + assert_projection(call, requests[i]) + raw = json.loads(record["provider_turns"][i]["wire"]["response_utf8"]) + assert {"thinking", "redacted_thinking"} <= { + block["type"] for block in raw["content"] + } + assert len({r.request_digest_sha256 for r in requests}) == len(requests) + for mutation in ("freeze_digest", "accumulate"): + bad = json.loads(json.dumps(calls[1])) + if mutation == "freeze_digest": + bad["system"] = calls[0]["system"] + else: + bad["messages"] = calls[0]["messages"] + bad["messages"] + with pytest.raises(AssertionError): + assert_projection(bad, requests[1]) + assert all(r["state"] == "settled" for r in budget.records.values()) + assert budget.cap is None + assert budget.totals()["remaining"] is None + assert ( + replay_trial(trial, record, expected_record_digest=record["record_digest"])[ + "provider_calls" + ] + == 0 + ) + assert ( + t.request_mapping + == registry(roster_profile=PROFILE, mode_profile="off-or-minimum-v1")[0][ + "mapping" + ] + ) + finally: + t.close() + budget.close() + + +@pytest.mark.parametrize("fault", ["multiple", "json", "usage", "model"]) +def test_sonnet_invalid_transport(tmp_path, fault): + from operatebench.agents.transport import ProviderFailure + from tools.three_flow_runtime import run_trial + + budget, t, trial, network = fixture( + tmp_path, "anthropic", MODEL, fault, roster_profile=PROFILE + ) + try: + with pytest.raises(ProviderFailure): + run_trial(trial, t, output=tmp_path / "trial") + assert len(network.transports[0].calls) == 1 + assert all(r["state"] == "unknown" for r in budget.records.values()) + finally: + t.close() + budget.close() + + +@pytest.mark.parametrize( + "fault", ["args", "badargs", "invalid_payload", "refusal", "no_action"] +) +def test_sonnet_quality_failure_no_salvage(tmp_path, fault): + from tools.three_flow_runtime import run_trial + + budget, t, trial, network = fixture( + tmp_path, "anthropic", MODEL, fault, roster_profile=PROFILE + ) + try: + record = run_trial(trial, t, output=tmp_path / "trial") + assert not record["evaluation"]["reliable"] + assert budget.totals()["unknown"] == 0 + assert network.transports[0].calls + finally: + t.close() + budget.close() + + +def test_november_published_profile_closed(): + from tests.test_selected_six_bounds import mistral_profile + from tools.three_flow_admission import digest, validate_profile + from tools.three_flow_profiles import ENDPOINTS + + p = mistral_profile() + p.update( + endpoint=ENDPOINTS["anthropic"], + context_max=1000000, + output_max=128000, + input_usd_per_million="4", + output_usd_per_million="10", + pricing_policy="november-published-token-envelope-v1", + ) + p["tariff_evidence"]["category_rates"] = { + "input": "2", + "cache_read": "0.10", + "cache_write": "4", + "output": "10", + } + p["rate_evidence_sha256"] = digest(p["tariff_evidence"]) + validate_profile(p, "anthropic", model=MODEL, roster_profile=PROFILE) + for field, value in [ + ("output_max", 4096), + ("network_timeout_seconds", 30), + ("input_usd_per_million", "2"), + ("execution_policy", "legacy-v1"), + ]: + with pytest.raises(ValueError): + validate_profile( + dict(p, **{field: value}), + "anthropic", + model=MODEL, + roster_profile=PROFILE, + ) + with pytest.raises(ValueError): + validate_profile( + p, "anthropic", model=MODEL, roster_profile="selected-six-http-v1" + ) + + +def test_november_fresh_source_bound_admission_and_one_shot(tmp_path, monkeypatch): + from tests.test_selected_six_bounds import mistral_profile + from tests.test_selected_six_http import Network + from tests.test_three_flow_live import cli, write + from tests.test_three_flow_live import fixture as admission_fixture + from tests.test_three_flow_optional_limits import consent + from tools import three_flow_live as live + from tools.three_flow_admission import digest, validate + from tools.three_flow_campaign import assignments + + sha = "d" * 40 + monkeypatch.setattr(live, "source_identity", lambda expected=None: sha) + path, a, _ = admission_fixture(tmp_path, "anthropic") + slot = next( + s for s in assignments(a["campaign_id"], PROFILE) if s["flow"] == "commerce" + ) + p = mistral_profile() + p.update( + endpoint="https://api.anthropic.com/v1/messages", + context_max=1000000, + output_max=128000, + input_usd_per_million="4", + output_usd_per_million="10", + pricing_policy="november-published-token-envelope-v1", + ) + p["tariff_evidence"]["category_rates"] = { + "input": "2", + "cache_read": "0.10", + "cache_write": "4", + "output": "10", + } + p["rate_evidence_sha256"] = digest(p["tariff_evidence"]) + a.update( + roster_profile=PROFILE, + mode_profile="off-or-minimum-v1", + registry=registry(roster_profile=PROFILE, mode_profile="off-or-minimum-v1"), + selected=[slot["trial_id"]], + pending=[slot["trial_id"]], + profiles={MODEL: p}, + cap_usd=None, + limit_policy={**consent(), "source_sha": sha}, + ) + assert validate(a, sha, synthetic=True) == a + with pytest.raises(ValueError): + validate(a, "e" * 40, synthetic=True) + write(path, a) + network = Network() + assert cli(path, a, network) == 0 + count = len(network.transports) + assert cli(path, a, network) == 2 + assert len(network.transports) == count diff --git a/tools/three_flow_admission.py b/tools/three_flow_admission.py index 73b1d57..e406526 100644 --- a/tools/three_flow_admission.py +++ b/tools/three_flow_admission.py @@ -30,6 +30,7 @@ GEMINI_ROSTER_PROFILE, GEMINI_UNCAPPED_PROFILE, META_ROSTER_PROFILE, + NOVEMBER_PROFILE, SELECTED_SIX_PROFILE, ) from tools.three_flow_profiles import minimum_effort as minimum_effort @@ -114,7 +115,7 @@ def proposal(sha: str, roster_profile: str = "legacy-v1") -> dict[str, Any]: ], } - if roster_profile == SELECTED_SIX_PROFILE: + if roster_profile in (SELECTED_SIX_PROFILE, NOVEMBER_PROFILE): result["cap_usd"] = None if roster_profile == GEMINI_ROSTER_PROFILE: result["cap_usd"] = None @@ -187,7 +188,12 @@ def _validate( fields.add("roster_profile") if ( admission["roster_profile"] - not in (LATEST_ROSTER_PROFILE, META_ROSTER_PROFILE, SELECTED_SIX_PROFILE) + not in ( + LATEST_ROSTER_PROFILE, + META_ROSTER_PROFILE, + SELECTED_SIX_PROFILE, + NOVEMBER_PROFILE, + ) or admission.get("mode_profile") != "off-or-minimum-v1" ): raise ValueError("explicit latest roster requires off-or-minimum profile") @@ -484,7 +490,7 @@ def validate_historical_identity(a: dict[str, Any]) -> None: # selected_registry has always omitted output_limit. Select the recorded # schema by the original roster; keep every other historical schema closed. string_fields = {"endpoint", "mapping", "settings_source"} - if a.get("roster_profile") != SELECTED_SIX_PROFILE: + if a.get("roster_profile") not in (SELECTED_SIX_PROFILE, NOVEMBER_PROFILE): string_fields.add("output_limit") fields = string_fields | { "provider", @@ -752,10 +758,10 @@ def validate_profile( model: str | None = None, roster_profile: str = "legacy-v1", ) -> None: - if roster_profile == SELECTED_SIX_PROFILE: + if roster_profile in (SELECTED_SIX_PROFILE, NOVEMBER_PROFILE): from tools.three_flow_selected import validate_published_profile - validate_published_profile(p, provider, model) + validate_published_profile(p, provider, model, roster_profile=roster_profile) return if provider == "gemini": from tools.three_flow_gemini import MODELS as gemini_models diff --git a/tools/three_flow_campaign.py b/tools/three_flow_campaign.py index 620ea20..1bb2eaf 100644 --- a/tools/three_flow_campaign.py +++ b/tools/three_flow_campaign.py @@ -39,6 +39,7 @@ from tools.three_flow_profiles import ( GEMINI_ROSTER_PROFILE, META_ROSTER_PROFILE, + NOVEMBER_PROFILE, SELECTED_SIX_PROFILE, ) from tools.three_flow_profiles import ( @@ -83,7 +84,8 @@ def preflight(roster_profile: str = "legacy-v1") -> dict[str, Any]: "source_digest": source_binding(), "source_commit": source_commit(), "global_cap_usd": None - if roster_profile in (GEMINI_ROSTER_PROFILE, SELECTED_SIX_PROFILE) + if roster_profile + in (GEMINI_ROSTER_PROFILE, SELECTED_SIX_PROFILE, NOVEMBER_PROFILE) else "990" if roster_profile == META_ROSTER_PROFILE else "1000", diff --git a/tools/three_flow_http.py b/tools/three_flow_http.py index 276f288..dd2fc1d 100644 --- a/tools/three_flow_http.py +++ b/tools/three_flow_http.py @@ -58,6 +58,7 @@ ) from tools.three_flow_profiles import ( MODE_PROFILES, + NOVEMBER_MODELS, SELECTED_SIX, CampaignSelection, http_mode_fields, @@ -348,6 +349,7 @@ def __init__( raise ValueError("new models require explicit off-or-minimum profile") if not isinstance(api_key, str) or not api_key: raise ValueError("HTTP provider requires a nonempty string API key") + self.november = (provider, model) in NOVEMBER_MODELS self.selected_six = selected self.mode_profile = mode_profile self.mode_fields = ( @@ -385,6 +387,8 @@ def __init__( self.request_mapping = ( "three-flow-anthropic-selected-six-single-decision-v2" ) + if self.november: + self.request_mapping = "three-flow-anthropic-november-stateless-v1" self.guard, self.max_output_tokens = guard, max_output_tokens self.wire = BudgetWire(inner, guard, ENDPOINTS[provider], operation=selection.api) self.wire.sensitive_response = api_key.encode() @@ -564,6 +568,14 @@ def __init__( self.settings["reasoning_mode"] = ( "MINIMUM" if model == "GLM-5.3" else "OFF" ) + if self.november: + self.settings.update( + tool_choice={"type": "auto", "disable_parallel_tool_use": True}, + native_continuation=False, + no_native_continuation=True, + connection_timeout_seconds=10, + pool_timeout_seconds=10, + ) self.last_turn: dict[str, Any] | None = None self.flight = SingleFlight(adapter="three-flow HTTP") @@ -618,19 +630,23 @@ def payload(self, request: ModelRequest) -> dict[str, Any]: **self.mode_fields, } if self.provider == "anthropic": + messages: list[dict[str, Any]] = [{"role": "user", "content": text}] + system = aa.request_instructions(request) return { "model": self.model, "max_tokens": request.max_output_tokens, - "system": aa.request_instructions(request), + "system": system, "tools": aa.anthropic_outcome_tools(), "tool_choice": ( {"type": "any", "disable_parallel_tool_use": True} if self.selected_six + else {"type": "auto", "disable_parallel_tool_use": True} + if self.november else {"type": "auto"} ), "thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}, - "messages": [{"role": "user", "content": text}], + "messages": messages, **self.mode_fields, } payload = { @@ -694,17 +710,19 @@ def _dispatch(self, payload: dict[str, Any]) -> tuple[Any, ModelResponse]: if self.provider == "anthropic": raw = self.client.messages.with_raw_response.create( **ap.anthropic_sdk_kwargs(payload), - timeout=self.http.timeout if self.selected_six else self.network_timeout, + timeout=self.http.timeout + if self.selected_six or self.november + else self.network_timeout, ) wire = WireResponse(raw.text(), raw.parse, kind="Fable campaign") # Validate hidden reasoning blocks, then use the unchanged legacy # validator on the semantic text/tool projection. Full raw retained. - if self.selected_six: + if self.selected_six or self.november: from tools.three_flow_selected import check_anthropic_reading check_anthropic_reading(wire) body = dict(wire.wire) - if self.selected_six: + if self.selected_six or self.november: body["usage"] = dict(body["usage"]) body["usage"].pop("speed", None) content = body.get("content") diff --git a/tools/three_flow_live.py b/tools/three_flow_live.py index 9b674e2..322dcd5 100644 --- a/tools/three_flow_live.py +++ b/tools/three_flow_live.py @@ -403,6 +403,7 @@ def main(argv: list[str] | None = None, *, network: Any = None) -> int: "legacy-v1", "latest-http-v1", "selected-six-http-v1", + "november-model-expansion-v1", "meta-standard-same44-v1", "gemini-standard-same44-low-v1", ), diff --git a/tools/three_flow_profiles.py b/tools/three_flow_profiles.py index f503880..ce82e7e 100644 --- a/tools/three_flow_profiles.py +++ b/tools/three_flow_profiles.py @@ -66,6 +66,9 @@ def minimum_effort(model: str) -> str: EXCLUDED_FUTURE_MODEL = "Qwen/Qwen3.5-122B-A10B-FP8" +NOVEMBER_PROFILE = "november-model-expansion-v1" +NOVEMBER_MODELS = (("anthropic", "claude-sonnet-5-5"),) + SELECTED_SIX_PROFILE = "selected-six-http-v1" SELECTED_SIX = ( ("openai", "gpt-6.1-terra"), @@ -82,6 +85,8 @@ def minimum_effort(model: str) -> str: def selected_roster(roster_profile: str = "legacy-v1") -> tuple[tuple[str, str], ...]: + if roster_profile == NOVEMBER_PROFILE: + return NOVEMBER_MODELS if roster_profile == SELECTED_SIX_PROFILE: return SELECTED_SIX if roster_profile == GEMINI_ROSTER_PROFILE: @@ -134,7 +139,9 @@ def selection_for( provider: str, model: str, mode_profile: str ) -> CampaignSelection | None: """Resolve only existing valid cells; callers retain their validation order.""" - if (provider, model) in SELECTED_SIX and mode_profile == "off-or-minimum-v1": + if mode_profile == "off-or-minimum-v1" and ( + (provider, model) in SELECTED_SIX + NOVEMBER_MODELS + ): return CampaignSelection( provider, model, @@ -173,6 +180,8 @@ def http_reasoning_mode(model: str) -> str: def http_mode_fields(provider: str, model: str, mode_profile: str) -> dict[str, Any]: + if (provider, model) in NOVEMBER_MODELS: + return {"thinking": {"type": "between_tools"}, "output_config": {"effort": "low"}} if (provider, model) in SELECTED_SIX: from tools.three_flow_selected import mode_fields @@ -240,6 +249,28 @@ def registry( transport_policy: str = "legacy-v1", roster_profile: str = "legacy-v1", ) -> list[dict[str, Any]]: + if roster_profile == NOVEMBER_PROFILE: + if mode_profile != "off-or-minimum-v1": + raise ValueError("November requires qualified minimum mode") + if transport_policy not in ("legacy-v1", "paced-safe-errors-v1"): + raise ValueError("unknown transport policy") + return [ + { + "provider": p, + "model": m, + "endpoint": ENDPOINTS[p], + "mapping": "three-flow-anthropic-november-stateless-v1", + "request_settings": { + **http_mode_fields(p, m, mode_profile), + "tool_choice": {"type": "auto", "disable_parallel_tool_use": True}, + "no_native_continuation": True, + }, + "settings_source": "tools/three_flow_http.py", + "retries": 0, + "admitted": False, + } + for p, m in NOVEMBER_MODELS + ] if roster_profile == SELECTED_SIX_PROFILE: if mode_profile != "off-or-minimum-v1": raise ValueError("selected six requires qualified minimum mode") diff --git a/tools/three_flow_selected.py b/tools/three_flow_selected.py index f2aa3d8..27871f6 100644 --- a/tools/three_flow_selected.py +++ b/tools/three_flow_selected.py @@ -10,7 +10,12 @@ from operatebench.agents.transport import ModelResponse, ToolCall from operatebench.providers.wire import WireResponse, wire_invalid -from tools.three_flow_profiles import ENDPOINTS, SELECTED_SIX +from tools.three_flow_profiles import ( + ENDPOINTS, + NOVEMBER_MODELS, + NOVEMBER_PROFILE, + SELECTED_SIX, +) def mode_fields(provider: str, model: str) -> dict[str, Any]: @@ -326,12 +331,20 @@ def check_river_wire(body: Any) -> None: } -def validate_published_profile(p: Any, provider: str, model: str | None) -> None: +def validate_published_profile( + p: Any, + provider: str, + model: str | None, + *, + roster_profile: str = "selected-six-http-v1", +) -> None: import re from decimal import Decimal from tools.three_flow_admission import GUARANTEES, digest + november = roster_profile == NOVEMBER_PROFILE + roster = NOVEMBER_MODELS if november else SELECTED_SIX fields = { "endpoint", "input_max", @@ -351,21 +364,22 @@ def validate_published_profile(p: Any, provider: str, model: str | None) -> None } if provider == "river_http": fields.add("owner_output_cap_tokens") - if ( - not isinstance(p, dict) - or set(p) != fields - or (provider, model) not in SELECTED_SIX - ): + if not isinstance(p, dict) or set(p) != fields or (provider, model) not in roster: raise ValueError("closed selected-six published profile required") if ( - p["pricing_policy"] != PRICING_POLICY + p["pricing_policy"] + != ("november-published-token-envelope-v1" if november else PRICING_POLICY) or p["accounting_policy"] != ACCOUNTING_POLICY or p["guarantees"] != dict.fromkeys(GUARANTEES, False) ): raise ValueError("selected-six estimate cannot assert billing guarantees") - validate_selected_profile(p, provider, model) + if november: + if p.get("execution_policy") != "three-flow-optional-limits-v1": + raise ValueError("November requires null monetary policy") + else: + validate_selected_profile(p, provider, model) assert model is not None - context, input_max, output = BOUNDS[model] + context, input_max, output = (1000000, None, 128000) if november else BOUNDS[model] if ( (p["context_max"], p["input_max"], p["output_max"]) != (context, input_max, output) @@ -373,7 +387,7 @@ def validate_published_profile(p: Any, provider: str, model: str | None) -> None or p["endpoint"] != ENDPOINTS[provider] ): raise ValueError("selected-six published route/bounds differ") - rates = TARIFFS[model] + rates = ("2", "0.10", "4", "10") if november else TARIFFS[model] if ( p["input_usd_per_million"] != str(max(Decimal(v) for v in rates[:3] if v is not None)) From 185aac7e40f21c05e28cb47b788520c27ed2ab30 Mon Sep 17 00:00:00 2001 From: Dmitry Khanukov Date: Thu, 5 Nov 2026 01:37:44 +0000 Subject: [PATCH 2/4] fix: align Sonnet transport timeouts with recorded settings Signed-off-by: Dmitry Khanukov --- tests/test_three_flow_november.py | 6 ++++++ tools/three_flow_http.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/tests/test_three_flow_november.py b/tests/test_three_flow_november.py index 996f8a5..c23c887 100644 --- a/tests/test_three_flow_november.py +++ b/tests/test_three_flow_november.py @@ -81,6 +81,12 @@ def capture(request): monkeypatch.setattr(t, "send", capture) try: + assert network.transports[0].calls == [] + assert t.http.timeout.connect == t.settings["connection_timeout_seconds"] == 10 + assert t.http.timeout.pool == t.settings["pool_timeout_seconds"] == 10 + assert ( + t.http.timeout.read is t.http.timeout.write is t.settings["timeout"] is None + ) record = run_trial(trial, t, output=tmp_path / "trial") assert record["evaluation"]["reliable"] assert t.settings["native_continuation"] is False diff --git a/tools/three_flow_http.py b/tools/three_flow_http.py index dd2fc1d..b9ce1ad 100644 --- a/tools/three_flow_http.py +++ b/tools/three_flow_http.py @@ -416,6 +416,8 @@ def __init__( ) ) self.client: Any + if self.november: + self.http.timeout = (10, None, None, 10) if provider in ("openai", "meta", "xai", "river_http"): self.client = explicit_sdk_client( openai.OpenAI, From 213d672ddaa8ec296c6b0bbe7b9c8e01785e020e Mon Sep 17 00:00:00 2001 From: Dmitry Khanukov Date: Thu, 5 Nov 2026 02:02:47 +0000 Subject: [PATCH 3/4] fix: reject unsupported November pacing policy Signed-off-by: Dmitry Khanukov --- tests/test_three_flow_november.py | 37 ++++++++++++++++++++++++++++++- tools/three_flow_profiles.py | 4 ++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/tests/test_three_flow_november.py b/tests/test_three_flow_november.py index c23c887..06df07c 100644 --- a/tests/test_three_flow_november.py +++ b/tests/test_three_flow_november.py @@ -29,6 +29,27 @@ def test_november_finite_roster_and_mapping(): assert preflight(PROFILE)["global_cap_usd"] is None +@pytest.mark.parametrize("transport_policy", [None, "legacy-v1"]) +def test_november_accepts_legacy_transport_policy(transport_policy): + kwargs = {} if transport_policy is None else {"transport_policy": transport_policy} + assert registry( + roster_profile=PROFILE, mode_profile="off-or-minimum-v1", **kwargs + ) == registry( + roster_profile=PROFILE, + mode_profile="off-or-minimum-v1", + transport_policy="legacy-v1", + ) + + +def test_november_rejects_paced_transport_policy(): + with pytest.raises(ValueError, match="November requires legacy-v1 transport policy"): + registry( + roster_profile=PROFILE, + mode_profile="off-or-minimum-v1", + transport_policy="paced-safe-errors-v1", + ) + + def assert_projection(call, request): from operatebench.agents import anthropic_messages as aa @@ -213,7 +234,10 @@ def test_november_published_profile_closed(): ) -def test_november_fresh_source_bound_admission_and_one_shot(tmp_path, monkeypatch): +@pytest.mark.parametrize("transport_policy", [None, "paced-safe-errors-v1"]) +def test_november_fresh_source_bound_admission_and_one_shot( + tmp_path, monkeypatch, transport_policy +): from tests.test_selected_six_bounds import mistral_profile from tests.test_selected_six_http import Network from tests.test_three_flow_live import cli, write @@ -255,6 +279,17 @@ def test_november_fresh_source_bound_admission_and_one_shot(tmp_path, monkeypatc cap_usd=None, limit_policy={**consent(), "source_sha": sha}, ) + if transport_policy is not None: + a["transport_policy"] = transport_policy + with pytest.raises( + ValueError, match="November requires legacy-v1 transport policy" + ): + validate(a, sha, synthetic=True) + write(path, a) + network = Network() + assert cli(path, a, network) == 2 + assert network.transports == [] + return assert validate(a, sha, synthetic=True) == a with pytest.raises(ValueError): validate(a, "e" * 40, synthetic=True) diff --git a/tools/three_flow_profiles.py b/tools/three_flow_profiles.py index ce82e7e..c0565dc 100644 --- a/tools/three_flow_profiles.py +++ b/tools/three_flow_profiles.py @@ -252,8 +252,8 @@ def registry( if roster_profile == NOVEMBER_PROFILE: if mode_profile != "off-or-minimum-v1": raise ValueError("November requires qualified minimum mode") - if transport_policy not in ("legacy-v1", "paced-safe-errors-v1"): - raise ValueError("unknown transport policy") + if transport_policy != "legacy-v1": + raise ValueError("November requires legacy-v1 transport policy") return [ { "provider": p, From 3e85af7be28c6869ef9b87eecd0b8adcc87ca2eb Mon Sep 17 00:00:00 2001 From: Dmitry Khanukov Date: Thu, 5 Nov 2026 03:27:37 +0000 Subject: [PATCH 4/4] test: initialize November mode in legacy HTTP fixture Signed-off-by: Dmitry Khanukov --- tests/test_http_null_metadata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_http_null_metadata.py b/tests/test_http_null_metadata.py index 11dde71..c2cff86 100644 --- a/tests/test_http_null_metadata.py +++ b/tests/test_http_null_metadata.py @@ -109,6 +109,7 @@ def handle(request): ) transport = object.__new__(HTTPCampaignTransport) transport.selected_six = False + transport.november = False transport.provider = provider transport.model = MODELS[provider] transport.client = client