From e14b28c114b4e432b96e9487b0434a39f74af100 Mon Sep 17 00:00:00 2001 From: rezaho Date: Fri, 11 Sep 2026 16:57:01 +0200 Subject: [PATCH] Pin the prompt-cache provider gate against its class-name fallback The gate reads the provider the factory stamped on the adapter and falls back to the adapter's own class name, which answers "openai" for the very class every unrecognized provider is routed to. Through the factory the stamp always decides and the fallback never runs, so a later edit to _provider_name could move which endpoints receive the fields without failing anything. Three tests state the matrix the gate is written against. A hand-built adapter carries no stamp and is taken at its class name, which is the intended reading: constructing OpenAIAdapter by hand says the request is bound for OpenAI's own endpoint, and the Azure subclass pins azure the same way. A stamped foreign provider closes the gate whichever class carries the stamp, which is the part a factory-built payload cannot show on its own, since the factory picks the class and writes the stamp together. And openrouter and xai, which the factory hands to their own builder, are pinned as receiving nothing. No behaviour change. --- tests/models/test_openai_prompt_cache.py | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/models/test_openai_prompt_cache.py b/tests/models/test_openai_prompt_cache.py index 462dd304..b86f888a 100644 --- a/tests/models/test_openai_prompt_cache.py +++ b/tests/models/test_openai_prompt_cache.py @@ -238,6 +238,50 @@ def test_the_two_measured_providers_do_get_the_fields(provider): assert payload["prompt_cache_options"] == OPTIONS +@pytest.mark.parametrize("provider", ["openrouter", "xai"]) +def test_a_provider_with_its_own_builder_is_never_handed_the_fields(provider): + """AC-1c over the rest of the factory table. These two route to + ``OpenRouterAdapter`` and get a chat-completions body of its own making, so the + routing withholds the fields here and the gate never runs. Pinned anyway: the claim + that has to stay true is "only openai and azure receive them", and that one spans + every provider the factory knows, however each is served.""" + adapter = ProviderAdapterFactory.create_adapter( + provider=provider, model_name="gpt-5.6-terra", + api_key="not-a-real-key", base_url="https://example.invalid/v1", + ) + payload = adapter.format_request_payload(CONVERSATION) + assert "prompt_cache_options" not in payload + assert "prompt_cache_breakpoint" not in json.dumps(payload) + + +def test_a_hand_built_adapter_is_taken_at_its_class_name(builder): + """The gate reads the provider the factory stamped and falls back to the class name + when nothing stamped one, and the fallback is a decision rather than an accident. + Building ``OpenAIAdapter`` by hand says the request is bound for OpenAI's own + endpoint; ``AzureOpenAIAdapter`` pins ``azure`` the same way. Both name a surface + the fields were measured on. Only the factory ever aims this class at a foreign + endpoint, and it always stamps. So a later edit to ``_provider_name`` has to move + this test before it can move which endpoints receive the fields.""" + adapter = _make(builder, "gpt-5.6-terra") + assert getattr(adapter, "provider", None) is None + payload = adapter.format_request_payload(CONVERSATION) + assert _markers(payload) + assert payload["prompt_cache_options"] == OPTIONS + + +@pytest.mark.parametrize("provider", ["xai", "groq", "some-new-gateway"]) +def test_a_stamped_provider_beats_the_class_name(builder, provider): + """AC-1c at the level the gate actually reads. Routing every unrecognized provider + to the OpenAI builder is only safe because the stamp outranks class identity, which + a payload built through the factory cannot show on its own — the factory picks the + class and writes the stamp together. Here they disagree, and the stamp wins.""" + adapter = _make(builder, "gpt-5.6-terra") + adapter.provider = provider + payload = adapter.format_request_payload(CONVERSATION) + assert "prompt_cache_options" not in payload + assert "prompt_cache_breakpoint" not in json.dumps(payload) + + # --- AC-2: the generation gate ------------------------------------------------------