diff --git a/tests/agent/test_error_classifier.py b/tests/agent/test_error_classifier.py index fcfa82747c..3cdf06e948 100644 --- a/tests/agent/test_error_classifier.py +++ b/tests/agent/test_error_classifier.py @@ -844,17 +844,20 @@ class TestClassifyApiError: e = MockAPIError("Error code: 400 - " + body["message"], status_code=400, body=body) assert classify_api_error(e, provider=provider, model="gpt-5.5").reason == expected - def test_azure_conflicting_continuation_identities_is_invalid_encrypted_content(self): - """Azure Foundry's wording for a rejected encrypted-reasoning replay (#105369); ``code`` is the - generic ``invalid_value``, so the message decides.""" - message = "Conflicting authenticated continuation identities." + @pytest.mark.parametrize(("provider", "model", "message", "code"), [ + ("azure-foundry", "gpt-6-astra", "Conflicting authenticated continuation identities.", "invalid_value"), + # Custom Responses endpoint wraps the replay rejection in a generic bad_request (#95834). + ("custom", "gpt-5.6", "The encrypted content could not be decrypted or parsed.", "bad_request"), + ], ids=["azure-continuation-identities", "custom-decrypted-or-parsed"]) + def test_message_only_replay_rejection_is_invalid_encrypted_content(self, provider, model, message, code): + """Endpoints whose ``code`` is generic; the message wording alone must decide.""" e = MockAPIError( f"Error code: 400 - {{'error': {{'message': '{message}', 'type': 'invalid_request_error', " - "'param': 'input', 'code': 'invalid_value'}}", + f"'param': 'input', 'code': '{code}'}}", status_code=400, - body={"error": {"message": message, "type": "invalid_request_error", "param": "input", "code": "invalid_value"}}, + body={"error": {"message": message, "type": "invalid_request_error", "param": "input", "code": code}}, ) - result = classify_api_error(e, provider="azure-foundry", model="gpt-6-astra") + result = classify_api_error(e, provider=provider, model=model) assert result.reason == FailoverReason.invalid_encrypted_content assert result.retryable is True assert result.should_fallback is False diff --git a/tests/agent/transports/test_codex_transport.py b/tests/agent/transports/test_codex_transport.py index 49eab87e82..03b7ded78c 100644 --- a/tests/agent/transports/test_codex_transport.py +++ b/tests/agent/transports/test_codex_transport.py @@ -478,6 +478,30 @@ class TestCodexBuildKwargs: assert messages[1]["codex_reasoning_items"][0]["encrypted_content"] == "sealed-1" assert messages[3]["codex_reasoning_items"][0]["encrypted_content"] == "sealed-2" + def test_normalize_response_stamps_wire_model_and_replay_drops_it_for_another_model(self, transport): + """The wire model from build_kwargs (``-900k`` stripped) is what normalize_response stamps, and a + later build_kwargs for another model on the same endpoint replays none of it (sealed blob → 400).""" + transport.build_kwargs(model="gpt-5.6-sol-900k", messages=[{"role": "user", "content": "hi"}], tools=[]) + normalized = transport.normalize_response(SimpleNamespace( + status="completed", + output=[ + SimpleNamespace(type="reasoning", id="rs_1", encrypted_content="sealed-sol", summary=[]), + SimpleNamespace(type="message", role="assistant", status="completed", id="msg_1", + content=[SimpleNamespace(type="output_text", text="done")]), + ], + )) + captured = normalized.codex_reasoning_items[0] + assert captured["_issuer_model"] == "gpt-5.6-sol" + history = [ + {"role": "user", "content": "hi"}, + {"role": "assistant", "content": "done", "codex_reasoning_items": [captured]}, + {"role": "user", "content": "next"}, + ] + same = transport.build_kwargs(model="gpt-5.6-sol-900k", messages=history, tools=[], replay_encrypted_reasoning=True) + other = transport.build_kwargs(model="gpt-5.7", messages=history, tools=[], replay_encrypted_reasoning=True) + assert [i["encrypted_content"] for i in same["input"] if i.get("type") == "reasoning"] == ["sealed-sol"] + assert not any(i.get("type") == "reasoning" for i in other["input"]) + def test_newest_reasoning_only_keeps_compaction_checkpoints(self): from agent.transports.codex import _newest_reasoning_only