Skip to main content

PikarcBlockedError

When Pikarc denies a run start, model call, or tool call, the SDK raises PikarcBlockedError. This happens before your function executes — no tokens are consumed, no tools are invoked.
from pikarc import AsyncPikarc, PikarcBlockedError

guard = AsyncPikarc(api_key="lg_...", base_url="http://localhost:8000")

try:
    async with guard.run(user_id="alice") as run:
        response = await run.model_call(
            fn=lambda: openai.chat.completions.create(...),
            model="gpt-4o",
        )
except PikarcBlockedError as e:
    print(e.reason)       # "WORKSPACE_DAILY_BUDGET_EXCEEDED"
    print(e.deny_reason)  # "WORKSPACE_DAILY_BUDGET_EXCEEDED"
finally:
    await guard.close()

Attributes

AttributeTypeDescription
reasonstrHuman-readable reason for the denial
deny_reasonstrMachine-readable deny reason code (see table below)

Deny Reason Codes

CodeDescriptionWhen Checked
KILL_SWITCH_ACTIVEThe workspace kill switch is enabled. All runs and steps are blocked.Run start, Step
USER_BLOCKEDThis SDK user has been individually blocked.Run start, Step
WORKSPACE_DAILY_BUDGET_EXCEEDEDTotal workspace spend today exceeds the configured daily budget.Run start, Step
USER_DAILY_BUDGET_EXCEEDEDThis user’s spend today exceeds the configured per-user daily budget.Run start, Step
MONTHLY_RUN_LIMIT_EXCEEDEDThe workspace has reached its monthly run limit (based on plan tier).Run start
MAX_CONCURRENT_RUNS_EXCEEDEDToo many runs are active simultaneously.Run start
RUN_ALREADY_ENDEDThe run has already been ended (COMPLETED or FAILED).Step

Where Denials Can Happen

At run start

When guard.run() is entered, Pikarc evaluates all 6 rules. If denied, the async with block is never entered:
try:
    async with guard.run(user_id="alice") as run:
        # This code never executes if run start is denied
        ...
except PikarcBlockedError as e:
    # Handle run-start denial
    print(f"Run blocked: {e.deny_reason}")

At step creation

When run.model_call() or run.tool_call() is called, rules 1-4 are re-evaluated. If denied, fn is never called:
async with guard.run(user_id="alice") as run:
    try:
        response = await run.model_call(
            fn=lambda: expensive_model_call(),
            model="gpt-4o",
        )
    except PikarcBlockedError as e:
        # The model was never called — no tokens consumed
        print(f"Step blocked: {e.deny_reason}")

HTTP Errors

The SDK uses httpx under the hood. Network errors or non-2xx responses from the Pikarc API will raise httpx.HTTPStatusError:
import httpx

try:
    async with guard.run(user_id="alice") as run:
        ...
except PikarcBlockedError:
    # Guardrail denial
    ...
except httpx.HTTPStatusError as e:
    # API error (401 unauthorized, 500 server error, etc.)
    print(f"API error: {e.response.status_code}")

Best Practices

  1. Always catch PikarcBlockedError — it’s expected behavior, not a bug. Return a user-friendly message.
  2. Always call guard.close() — use a finally block or async context manager to clean up the HTTP client.
  3. Catch at the right level — catch around guard.run() for run-start denials, or around individual model_call()/tool_call() for step-level denials.
  4. Log the deny reasone.deny_reason tells you exactly which guardrail triggered. Use it for monitoring and alerting.