BerriAI/litellm · error · OCIError

oci_key must be a string containing the PEM private key cont

Error message

oci_key must be a string containing the PEM private key content. Got type: {type(oci_key).__name__}

What it means

In the manual signing path, if oci_key is truthy it must be a str containing PEM content; any other type (bytes, dict, path-like, list) raises OCIError(400) naming the offending type. The check exists because serialized keys (e.g. read as bytes from a vault) would otherwise fail later with a confusing serialization error.

Source

Thrown at litellm/llms/oci/common_utils.py:307

    }

    signed_header_names: Final = [
        "date",
        "(request-target)",
        "host",
        "content-length",
        "content-type",
        "x-content-sha256",
    ]
    signing_string: Final = build_signature_string(method, path, headers_to_sign, signed_header_names)

    _require_cryptography()

    # Resolve the private key — prefer inline PEM content over file path
    oci_key_content: str | None = None
    if oci_key:
        if not isinstance(oci_key, str):
            raise OCIError(
                status_code=400,
                message=(
                    f"oci_key must be a string containing the PEM private key content. "
                    f"Got type: {type(oci_key).__name__}"
                ),
            )
        oci_key_content = oci_key.replace("\\n", "\n").replace("\r\n", "\n")

    private_key: Final = (
        load_private_key_from_str(oci_key_content)
        if oci_key_content
        else load_private_key_from_file(oci_key_file)
        if oci_key_file
        else None
    )

    if private_key is None:
        raise OCIError(

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Decode bytes to str: oci_key=key_bytes.decode('utf-8').
  2. If the value is a path, use oci_key_file (or OCI_KEY_FILE) instead of oci_key.
  3. If the secret is JSON-wrapped, extract the PEM field: json.loads(secret)['pem'].
  4. Confirm the value starts with '-----BEGIN' and ends with '-----END ... KEY-----'.

Example fix

# before
optional_params["oci_key"] = secret_manager.get("oci_key")  # bytes → OCIError 400

# after
key = secret_manager.get("oci_key")
if isinstance(key, bytes):
    key = key.decode("utf-8")
optional_params["oci_key"] = key
Defensive patterns

Strategy: type-guard

Validate before calling

key = optional_params.get("oci_key")
if isinstance(key, bytes):
    key = key.decode("utf-8")
assert key is None or (isinstance(key, str) and "-----BEGIN" in key), "oci_key must be PEM string content"

Type guard

from typing import Any

def is_pem_key_string(v: Any) -> bool:
    return isinstance(v, str) and v.lstrip().startswith("-----BEGIN")

Try / catch

from litellm.llms.oci.common_utils import OCIError
try:
    litellm.completion(model="oci/...", messages=m)
except OCIError as e:
    if e.status_code == 400 and "oci_key must be a string" in str(e):
        optional_params["oci_key"] = optional_params["oci_key"].decode("utf-8")
    raise

Prevention

When it happens

Trigger: Passing oci_key=b'-----BEGIN PRIVATE KEY-----...' (bytes from AWS Secrets Manager / Vault SDKs), an OCI config dict, or accidentally passing the key *file path* as oci_key instead of its contents.

Common situations: Secret managers returning bytes; JSON-encoded secrets passed as parsed dicts; confusing oci_key (inline PEM string) with oci_key_file (path) and passing a path to oci_key.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/78d9a90a389647dc. Report an issue: GitHub.