Skip to main content

SDK errors

The SDK surfaces metadata for exceptions. All errors inherit from perceptron.errors.SDKError.
Error / err.codeTriggersSuggested next step
AuthError (auth)Missing/invalid API key, revoked org accessRe-export PERCEPTRON_API_KEY, double-check PERCEPTRON_PROVIDER/BASE_URL, share the request_id.
BadRequestError (bad_request)Invalid payloads, malformed prompts, unsupported paramsInspect err.details["message"] / offending field; fix input before retrying.
ExpectationError (expectation_failed)Strict-mode validation (boxes missing anchors, malformed coordinates)Keep prompts/images ordered, attach image= handles, validate coordinates with result.points_to_pixels().
AnchorError (anchor_missing, bounds_out_of_range)Dedicated anchor violations exposed via strict modeAdd explicit anchors or clamp/rescale boxes; log err.details["code"].
RateLimitError (rate_limit)429 from API or providerRead err.details["retry_after"], back off exponentially, log request_id.
TimeoutError (timeout)Client-side deadline exceededIncrease config(timeout=...), trim uploads, consider stream=True.
TransportError (transport)Network failures (DNS, TLS, broken pipes)Retry with jitter; if persistent, verify outbound firewall / proxy.
ServerError (server_error)5xxs or malformed upstream responsesRetry automatically up to your policy; include request_id when contacting support.
Non-fatal semantic issues still land in PerceiveResult.errors; flip strict=True when you want them raised immediately during prompting and pointing.

Retrying safely

Wrap SDK calls with targeted exception handling so transient faults (rate limits, network blips) never crash your job while still surfacing actionable diagnostic info.
from perceptron import caption
from perceptron.errors import SDKError, RateLimitError
import logging, random, time

logger = logging.getLogger(__name__)

def safe_caption(path):
    for attempt in range(5):
        try:
            return caption(path, style="concise")
        except RateLimitError as err:
            wait = err.details.get("retry_after") or 2 ** attempt + random.random()
            logger.warning(
                "Rate limited (request_id=%s). Retrying in %.1fs",
                err.details.get("request_id"),
                wait,
            )
            time.sleep(wait)
        except SDKError as err:
            logger.error("Perceptron error %s: %s", err.code or "unknown", err)
            logger.debug("details=%s", err.details)
            raise

    raise RuntimeError("Exceeded retries calling caption()")

API error codes

API responses bubble up through the SDK as the error classes above. Use the table below to match symptoms (HTTP status, timeout, etc.) to the remediation you need in client code.
Symptom: 401 Unauthorized from API or SDK calls.Fix: Export PERCEPTRON_API_KEY (and PERCEPTRON_BASE_URL when self-hosting) or call configure(api_key="sk_live_..."). Share err.details.get("request_id") with support if the failure persists.
Symptom: Key is valid but lacks scope.Fix: Rotate/regenerate the key in the dashboard and ensure the organization has access to the requested provider (perceptron, fal, etc.).
Symptom: Too many requests per minute.Fix: Batch requests and implement exponential backoff using the surfaced retry_after seconds.
from perceptron.errors import RateLimitError
import random, time

def call_with_backoff(fn, *args, **kwargs):
    for attempt in range(5):
        try:
            return fn(*args, **kwargs)
        except RateLimitError as err:
            wait = err.details.get("retry_after") or 2 ** attempt + random.random()
            time.sleep(wait)
    raise RuntimeError("Exceeded retries after RateLimitError")
Symptom: Immediate BadRequestError, ExpectationError, or warnings in PerceiveResult.errors.Fix: Inspect err.details (provider message, offending field, request ID) or switch on strict=True to surface anchor issues early.
Symptom: Transient backend issue.Fix: Retry with jitter and log err.details.get("request_id") when opening support tickets.
Symptom: Long-running uploads or unstable network.Fix: Resize oversized images (<=1024px longest edge), prefer stream=True to receive incremental tokens, and reduce config(timeout=...) so hung connections fail fast.

Decision trees with runnable repros

invalid_image (upload/source bytes)

  • Meaning: the SDK could not decode the bytes you pointed it at before the request ever left your machine.
  • Fix checklist:
    1. Make sure the path/bytes you pass into caption()/detect()/ocr() is a real bitmap (PNG/JPEG/WebP). When dealing with streams, flush the handle before re-reading it.
    2. Inspect err.details['origin'] to confirm which file triggered the failure and err.details['request_id'] (if the request made it to the API).
    3. If the asset lives on disk, open it with Pillow or Preview to confirm it is not truncated; if it is an HTTP URL, download it locally and retry.
from perceptron import caption
from perceptron.errors import SDKError

try:
    caption("/path/to/bad.docx", expects="text")  # intentionally not an image
except SDKError as err:
    origin = err.details.get("origin")
    request_id = err.details.get("request_id")
    print(f"Origin={origin}, request_id={request_id}")
    raise

anchor_missing / bounds_out_of_range (strict pointing)

  • Meaning: a box()/point()/polygon() tag is missing an explicit image= reference when multiple images exist, or the coordinates fall outside the detected width/height.
  • Fix checklist:
    1. When using @perceive(..., strict=True) always pass the image node into every tag: box(..., image=image_node). In single-image prompts, ensure the image appears before any tags.
    2. Use result.points_to_pixels(width, height) to sanity-check coordinate math before switching to strict mode.
    3. Log err.details['code'] (either anchor_missing or bounds_out_of_range) to branch your retry logic.
from perceptron import box, perceive, text
from perceptron.errors import AnchorError

@perceive(expects="box", strict=True)
def missing_anchor():
    return text("Mark the defect") + box(0, 0, 10, 10)

try:
    missing_anchor()
except AnchorError as err:
    print(err.code, err.details)