ci(oci): fix CI failures — black formatting + recursive_detector ignore

- Run black on litellm/llms/oci/common_utils.py + 3 OCI test files
  that drifted out of black-compliance during the rebase.
- Add the three bounded recursive functions in oci/common_utils.py
  (`_resolve`, `resolve_oci_schema_anyof`, `sanitize_oci_schema`) to
  the recursive_detector IGNORE_FUNCTIONS list. All three are bounded:
  `_resolve` uses a `resolving_stack` cycle guard; the other two are
  bounded by JSON-schema tree depth (no cycles in well-formed input),
  matching the pattern of the existing OCI/Vertex schema walkers
  already on the list.
This commit is contained in:
Federico Kamelhar 2026-05-05 23:48:09 -04:00
parent c04cf12cf2
commit 9f3f10ca1d
5 changed files with 46 additions and 31 deletions

View file

@ -96,7 +96,9 @@ def sha256_base64(data: bytes) -> str:
# OCI HTTP signing specification (RSA-SHA256 request signing), not for password
# or secret hashing. This is the correct and mandated algorithm for this purpose.
# See: https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm
digest = hashlib.sha256(data).digest() # lgtm[py/weak-sensitive-data-hashing] # noqa: S324
digest = hashlib.sha256(
data
).digest() # lgtm[py/weak-sensitive-data-hashing] # noqa: S324
return base64.b64encode(digest).decode()

View file

@ -47,6 +47,9 @@ IGNORE_FUNCTIONS = [
"_read_image_bytes", # max depth set.
"_get_masked_values", # max depth set (default 20) to prevent infinite recursion while masking nested sensitive config dicts.
"_redact_sensitive_litellm_params", # max depth set (default 10).
"_resolve", # OCI: $ref resolver bounded by `resolving_stack` cycle guard.
"resolve_oci_schema_anyof", # OCI: bounded by JSON-schema tree depth (no cycles possible in well-formed input).
"sanitize_oci_schema", # OCI: bounded by JSON-schema tree depth.
]

View file

@ -766,14 +766,24 @@ class TestCohereStreamChunkEdgeCases:
def test_stream_chunk_max_tokens_finish_reason(self):
wrapper = self._wrapper()
chunk = {"apiFormat": "COHERE", "text": "truncated", "index": 0, "finishReason": "MAX_TOKENS"}
chunk = {
"apiFormat": "COHERE",
"text": "truncated",
"index": 0,
"finishReason": "MAX_TOKENS",
}
result = wrapper.chunk_creator(f"data: {json.dumps(chunk)}")
assert result.choices[0].finish_reason == "length"
def test_stream_chunk_unknown_finish_reason_does_not_raise(self):
from litellm.llms.oci.chat.cohere import handle_cohere_stream_chunk
chunk = {"apiFormat": "COHERE", "text": "", "index": 0, "finishReason": "FUTURE_REASON"}
chunk = {
"apiFormat": "COHERE",
"text": "",
"index": 0,
"finishReason": "FUTURE_REASON",
}
# Should not raise — unknown reasons fall through the elif chain unchanged
result = handle_cohere_stream_chunk(chunk)
assert result.choices[0] is not None

View file

@ -25,7 +25,6 @@ from litellm.llms.oci.common_utils import (
validate_oci_environment,
)
# ---------------------------------------------------------------------------
# OCI_API_VERSION
# ---------------------------------------------------------------------------
@ -318,11 +317,7 @@ def test_resolve_schema_anyof_no_anyof_unchanged():
def test_resolve_schema_anyof_nested():
schema = {
"properties": {
"age": {"anyOf": [{"type": "integer"}, {"type": "null"}]}
}
}
schema = {"properties": {"age": {"anyOf": [{"type": "integer"}, {"type": "null"}]}}}
result = resolve_oci_schema_anyof(schema)
assert result["properties"]["age"]["type"] == "integer"

View file

@ -39,7 +39,6 @@ from litellm.llms.oci.common_utils import (
)
from litellm.types.llms.oci import OCIVendors, OCIToolCall
# ---------------------------------------------------------------------------
# Helpers / fixtures
# ---------------------------------------------------------------------------
@ -67,7 +66,9 @@ _GENERIC_MODEL = "meta.llama-3-70b-instruct"
@patch("litellm.llms.oci.common_utils.load_private_key_from_str")
@patch("litellm.llms.oci.common_utils.padding")
@patch("litellm.llms.oci.common_utils.hashes")
def test_sign_with_manual_credentials_inline_key(mock_hashes, mock_padding, mock_load_key):
def test_sign_with_manual_credentials_inline_key(
mock_hashes, mock_padding, mock_load_key
):
"""sign_with_manual_credentials succeeds with an inline oci_key string."""
mock_key = MagicMock()
mock_key.sign.return_value = b"fake_signature"
@ -88,7 +89,9 @@ def test_sign_with_manual_credentials_inline_key(mock_hashes, mock_padding, mock
@patch("litellm.llms.oci.common_utils.load_private_key_from_file")
@patch("litellm.llms.oci.common_utils.padding")
@patch("litellm.llms.oci.common_utils.hashes")
def test_sign_with_manual_credentials_key_file(mock_hashes, mock_padding, mock_load_file):
def test_sign_with_manual_credentials_key_file(
mock_hashes, mock_padding, mock_load_file
):
"""sign_with_manual_credentials falls back to oci_key_file when oci_key absent."""
mock_key = MagicMock()
mock_key.sign.return_value = b"sig_from_file"
@ -117,9 +120,7 @@ def test_sign_with_manual_credentials_authorization_contains_key_id(
mock_key.sign.return_value = b"sig"
mock_load_key.return_value = mock_key
result_headers, _ = sign_with_manual_credentials(
{}, _MANUAL_CREDS, {}, _API_BASE
)
result_headers, _ = sign_with_manual_credentials({}, _MANUAL_CREDS, {}, _API_BASE)
auth = result_headers["authorization"]
assert 'keyId="ocid1.tenancy.oc1..xxx/ocid1.user.oc1..xxx/aa:bb:cc:dd"' in auth
@ -143,9 +144,7 @@ def test_sign_oci_request_routes_to_signer_when_present():
"""sign_oci_request delegates to sign_with_oci_signer when oci_signer is set."""
signer = MagicMock()
signer.do_request_sign.return_value = None
headers, body = sign_oci_request(
{}, {"oci_signer": signer}, {"data": 1}, _API_BASE
)
headers, body = sign_oci_request({}, {"oci_signer": signer}, {"data": 1}, _API_BASE)
signer.do_request_sign.assert_called_once()
assert isinstance(body, bytes)
@ -229,7 +228,10 @@ def test_adapt_generic_assistant_tool_call_message():
{
"id": "call_xyz",
"type": "function",
"function": {"name": "get_weather", "arguments": '{"city": "Rome"}'},
"function": {
"name": "get_weather",
"arguments": '{"city": "Rome"}',
},
}
],
}
@ -249,7 +251,10 @@ def test_adapt_generic_multipart_content():
"role": "user",
"content": [
{"type": "text", "text": "Look at this:"},
{"type": "image_url", "image_url": {"url": "https://example.com/img.png"}},
{
"type": "image_url",
"image_url": {"url": "https://example.com/img.png"},
},
],
}
]
@ -712,17 +717,13 @@ class TestOCIChatConfigGetOptionalParams:
def test_cohere_maps_stop_to_stop_sequences(self):
config = self._config()
result = config._get_optional_params(
OCIVendors.COHERE, {"stop": ["END"]}
)
result = config._get_optional_params(OCIVendors.COHERE, {"stop": ["END"]})
assert "stopSequences" in result
assert result["stopSequences"] == ["END"]
def test_generic_maps_max_tokens(self):
config = self._config()
result = config._get_optional_params(
OCIVendors.GENERIC, {"max_tokens": 512}
)
result = config._get_optional_params(OCIVendors.GENERIC, {"max_tokens": 512})
assert result["maxTokens"] == 512
def test_tool_choice_string_auto_converted_to_dict(self):
@ -870,16 +871,20 @@ class TestOCIStreamWrapperChunkCreator:
def test_cohere_chunk_dispatched_correctly(self):
wrapper = self._make_wrapper(_COHERE_MODEL)
payload = json.dumps({"apiFormat": "COHERE", "text": "hi", "finishReason": None})
payload = json.dumps(
{"apiFormat": "COHERE", "text": "hi", "finishReason": None}
)
result = wrapper.chunk_creator(f"data:{payload}")
assert result.choices[0].delta.content == "hi"
def test_generic_chunk_dispatched_correctly(self):
wrapper = self._make_wrapper(_GENERIC_MODEL)
payload = json.dumps({
"finishReason": "COMPLETE",
"index": 0,
})
payload = json.dumps(
{
"finishReason": "COMPLETE",
"index": 0,
}
)
result = wrapper.chunk_creator(f"data:{payload}")
assert result.choices[0].finish_reason == "stop"