Skip to content
Docs navigation: System Zero / Request policy

Request policy

RightNow AIRightNow System ZeroRequest policy Updated

On this page

Add an optional policy object to a RightNow System Zero decide request. Answers below your confidence threshold come back without a decision, and a request whose maximum possible cost exceeds your cap is refused before any work.

decide.sh
# Set SYSTEM_ZERO_API_KEY to your API key before running this example.
# Generate once per logical decision; reuse this key when retrying.
idempotency_key="$(uuidgen 2>/dev/null \
  || cat /proc/sys/kernel/random/uuid 2>/dev/null \
  || openssl rand -hex 16 2>/dev/null \
  || python3 -c 'from uuid import uuid4; print(uuid4())' 2>/dev/null)"
cat > request.json <<'EOF'
{
  "model": "system-zero",
  "state": "The blender I ordered arrived with a cracked jar. I need it working before Saturday.",
  "questions": {
    "damaged_in_transit": {
      "type": "boolean",
      "instructions": "Did the item arrive damaged?"
    },
    "team": {
      "type": "choice",
      "instructions": "Which team should handle this ticket?",
      "criteria": {
        "replacements": "Ship a new item or part",
        "refunds": "Return money to the customer",
        "support": null
      }
    },
    "urgency": {
      "type": "score",
      "instructions": "How urgent is this ticket?",
      "criteria": [
        "Routine",
        "Priority",
        "Immediate"
      ]
    }
  },
  "policy": {
    "min_confidence": 0.8,
    "on_low_confidence": "abstain",
    "max_cost_usd": 0.29
  }
}
EOF
# Headerless error responses require Python 3 to read error.code.
# Manual example policy: 130 seconds per attempt, a 600-second resend window
# with no retry-count cap. If x-should-retry is present, resend only when
# x-should-retry is true. If the header is absent, resend on 429, or on 409 and
# 5xx only with a retryable error.code. No answer also retries. Wait Retry-After
# plus up to 10 %, otherwise a random wait doubling from 1 s to 30 s. Stop if
# Retry-After is over 60 s. Also stop when a 429 has no Retry-After, or a 5xx
# has x-should-retry but no Retry-After.
# SDK defaults: The default is 5 retries, also the maximum, with 150 seconds per
# attempt and a 60-second total retry budget. A single wait longer than 60
# seconds raises the last error.
# Use Retry-After-Ms, or Retry-After when milliseconds are absent. The wait is
# never shorter than a random backoff floor: a random fraction of 0.5 seconds
# doubled for each attempt, capped at 8 seconds.
random() { od -An -N4 -tu4 /dev/urandom | tr -d ' '; }
# Stop resending after 10 minutes.
start=$(date +%s) attempt=0
while :; do
  : "${SYSTEM_ZERO_API_KEY:?set it first}"
  : "${idempotency_key:?could not make one}"
  rm -f reply.json
  out=$(curl https://api.rightnowai.co/v1/system-zero/decide \
    --max-time 130 -sS -o reply.json -D - -w '%{http_code}' \
    -H "Authorization: Bearer $SYSTEM_ZERO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $idempotency_key" \
    -d @request.json)
  code=$(printf '%s' "$out" | tail -c 3)
  retry_header=$(printf '%s\n' "$out" \
    | grep -i '^x-should-retry:' | tail -n 1)
  retry=$(printf '%s\n' "$retry_header" \
    | sed 's/^[^:]*:[[:space:]]*//;s/[[:space:]]*$//')
  after=$(printf '%s\n' "$out" | grep -i '^retry-after:' \
    | tail -n 1 | sed 's/^[^:]*:[[:space:]]*//;s/[[:space:]]*$//')
  case $after in *[!0-9]*|'') after= ;; esac
  if [ -z "$retry_header" ]; then
    case $code in
      000|429) ;;
      409|5??)
        error_code=$(python3 -c 'import json, sys
try:
    print(json.load(sys.stdin).get("error", {}).get("code", ""))
except (ValueError, AttributeError):
    pass' < reply.json 2>/dev/null) || break
        case $code:$error_code in
          409:idempotency_conflict) ;;
          5??:service_unavailable) ;;
          5??:system_zero_unavailable) ;;
          5??:billing_settlement_error) ;;
          5??:idempotency_unavailable) ;;
          5??:limiter_unavailable) ;;
          5??:internal_error) ;;
          5??:maintenance) ;;
          *) break ;;
        esac ;;
      *) break ;;
    esac
  else
    [ "$retry" = true ] || break
    case $code in 4??|5??) ;; *) break ;; esac
  fi
  if [ -n "$after" ]; then
    after=$(printf '%s' "$after" | sed 's/^0*//')
    after=${after:-0}
    [ "$after" -le 60 ] || break
    ms=$(( after * 1000 + $(random) % (after * 100 + 1) ))
  else
    [ "$code" != 429 ] || break
    if [ -n "$retry_header" ]; then
      case $code in 5??) break ;; esac
    fi
    cap=$(( attempt < 5 ? 1000 << attempt : 30000 ))
    ms=$(( $(random) % (cap + 1) ))
  fi
  [ $(( $(date +%s) - start + ms / 1000 )) -lt 600 ] || break
  sleep "$(( ms / 1000 )).$(printf '%03d' $(( ms % 1000 )))"
  [ $(( $(date +%s) - start )) -lt 600 ] || break
  attempt=$(( attempt + 1 ))
done
echo "HTTP $code"
cat reply.json 2>/dev/null

Policy fields

min_confidence
Optional. A finite number greater than 0 and at most 1. An answer whose confidence is below it abstains; equal confidence passes.
on_low_confidence
Optional. Only abstain is accepted. An omitted value also abstains.
max_cost_usd
Optional. A cap in US dollars, greater than 0 and at most 1,000, with at most eight decimal places.

Every member is optional, and an empty object is valid. Send policy in a decide request or in a stream's start request. The gateway applies it; the model never receives it.

on_low_confidence: "error" and require_calibrated are not available. A request that sets either, including require_calibrated: false, returns 400 invalid_request.

The published SDKs have no policy support: their request types omit policy, and decisionsFor and decisions_for refuse an abstained answer. Send policy over HTTP and read answers yourself, as this guide shows. Playground runs send no policy.

Validation

An invalid policy returns 400 invalid_request with type invalid_request_error and error.retryable: false. A null policy, numeric strings, values that are not finite, wrong types and unknown members are invalid.

error.param is policy for a value that is not an object, policy.<member> for a known member and policy.* for an unknown one. A decision path that takes no policy refuses one with error.param policy.

The cap allows at most eight decimal places after exponent expansion, and trailing zeros do not count: 0.29, 0.07 and 3e-8 are valid, and 1e-9 is not. The gateway reads the original JSON number, so digits that a JSON parser would round away still count.

Abstained answers

After the model replies, the gateway compares each answer's confidence with min_confidence. An answer below the threshold keeps its other fields as evidence, loses decision and gains abstained: true and abstention_reason: "low_confidence".

Passing answers and question refusals keep their usual shape. A successful reply echoes the policy you sent, with no defaults added. In this illustrative reply, damaged_in_transit passes and the two other answers abstain.

answers and policy
JSON
{
  "answers": {
    "damaged_in_transit": {
      "type": "boolean",
      "probability": 0.93,
      "confidence": 0.93,
      "calibration_version": "unpublished",
      "decision": true,
      "alternatives": [
        {
          "decision": false,
          "confidence": 0.07
        }
      ],
      "calibrated": false
    },
    "team": {
      "type": "choice",
      "choice": "replacements",
      "probabilities": {
        "replacements": 0.71,
        "refunds": 0.21,
        "support": 0.08
      },
      "confidence": 0.71,
      "calibration_version": "unpublished",
      "alternatives": [
        {
          "decision": "refunds",
          "confidence": 0.21
        },
        {
          "decision": "support",
          "confidence": 0.08
        }
      ],
      "calibrated": false,
      "abstained": true,
      "abstention_reason": "low_confidence"
    },
    "urgency": {
      "type": "score",
      "score": 1.48,
      "legend": {
        "0": "Routine",
        "1": "Priority",
        "2": "Immediate"
      },
      "probabilities": {
        "0": 0.07,
        "1": 0.38,
        "2": 0.55
      },
      "confidence": 0.55,
      "calibration_version": "unpublished",
      "alternatives": [
        {
          "decision": "1",
          "confidence": 0.38
        },
        {
          "decision": "0",
          "confidence": 0.07
        }
      ],
      "calibrated": false,
      "abstained": true,
      "abstention_reason": "low_confidence"
    }
  },
  "policy": {
    "min_confidence": 0.8,
    "on_low_confidence": "abstain",
    "max_cost_usd": 0.29
  }
}

Never act on an abstained answer. Its choice, score, probability, probabilities and alternatives are model evidence, not a decision. Check abstained before you read decision.

Cost cap

Before admission, the gateway compares the request's maximum possible cost with max_cost_usd. It converts the cap exactly, at 100,000,000 microcents per US dollar, and the bound includes media minimums. Equal cost passes.

A request over the cap returns 400 policy_max_cost_exceeded before any model work. It is not counted toward monthly usage and is not charged. The bound can exceed the final cost, so a cap near the expected cost can refuse a request that would have cost less.

error.json
JSON
{
  "error": {
    "message": "The request's maximum possible cost exceeds max_cost_usd. The request was not dispatched, counted toward monthly usage or charged.",
    "type": "invalid_request_error",
    "param": "policy.max_cost_usd",
    "code": "policy_max_cost_exceeded",
    "retryable": false,
    "details": {
      "policy": {
        "max_cost_usd": 0.0001
      },
      "counted": false,
      "estimated_cost_microcents": 120000,
      "max_cost_microcents": 10000
    }
  },
  "request_id": "request-id-from-response"
}
error.details.counted
false: nothing was counted or charged.
error.details.policy
The policy you sent.
error.details.estimated_cost_microcents
The maximum possible cost of the request, rounded up to whole microcents.
error.details.max_cost_microcents
Your cap in microcents.

This refusal has error.retryable: false. Raise max_cost_usd or send a smaller request. The amounts in this example are illustrative.

Usage and billing

An abstained answer is charged exactly like any answered question. It counts toward the monthly cap and pays the normal per-answer question fee, alongside the request's other charges. See Pricing.

Question refusals stay unanswered and are not charged. A request refused by the cost cap is neither counted nor charged.

Streams

In a stream, send policy inside start.request. It stays fixed across state changes, and a mutation cannot alter it. A policy error arrives as an error event with fatal: false and does not close the stream. See Streaming.

Replay and stored data

Policy is part of the Idempotency-Key fingerprint. Changing the policy under the same key returns 400 idempotency_key_reuse.

A keyed replay returns the original result, abstentions included, without new model work, counting or charges. See Idempotency.

Stored replies keep their policy and abstentions under the same 24-hour answer window and eviction limits as other replies. Usage records keep only a count of abstained questions, with no policy, threshold, question name, reason or answer.