Handle a refused spend
PHPThe errors that say a budget, a limit or the credits of a customer refused a run, the two forms such a refusal comes in, and what to do about each.
3 min read
Slen checks a model call of a flow against the limits you set: the budgets of the project and the credits of your customers. When one of them says no, the call is refused before the provider is called. This page is about catching that refusal and acting on it.
The family of a refused spend
A refusal of this kind is answered with the status 402. Both SDKs raise it as one family: SpendRefusedException in PHP and SpendRefusedError in Node.js, and each code has a type of its own that extends it. A 402 with a code the SDK has no type for is raised as the family type itself.
| Code | HTTP status | SDK error type | What it means | What to do |
|---|---|---|---|---|
budget_exceeded | 402 | BudgetExceededException PHPBudgetExceededError Node.js | The call would take a budget over its limit. What the budget already holds and has spent in its current window, together with the hold this call asks for, is more than the limit. | Wait for the next window of the budget — its resets_at says when; a budget of the type total does not start again. Or raise the limit of the budget. |
operation_limit_exceeded | 402 | OperationLimitExceededException PHPOperationLimitExceededError Node.js | A per-operation budget caps what one call of the operation may hold, and the hold this call asks for is above that cap. | Ask for less — fewer output tokens, a cheaper model — or raise the cap. The same request is refused again until one of them changes. |
concurrency_limit_exceeded | 402 | ConcurrencyLimitException PHPConcurrencyLimitError Node.js | A budget of the type concurrency — Concurrent holds in the console — allows a set number of holds to be open at the same time, and all of them are taken by calls that are running. | Send the request again once a running call has ended, or raise the limit of the budget. A request to a flow goes again under a new key, since its rejected execution keeps the key it had. This is not the rate limit of the key. |
insufficient_credits | 402 | InsufficientCreditsException PHPInsufficientCreditsError Node.js | The credit account of the customer cannot cover the price of the call in credits, or the customer has no account of that credit type. A charge or an adjustment is refused the same way when it would leave the balance below what the account holds. | Top up the credits of the customer. An account whose settings allow a negative balance does not refuse. |
overrun_stopped | 402 | OverrunStoppedException PHPOverrunStoppedError Node.js | The estimate of a model call in a flow, made again after its hold was taken and before the provider is called, came out larger than the hold by more than the overrun tolerance of the node, and the overrun policy of the model node is to stop. The hold was given back, and the provider was not called. The same code ends the execution when a person refused the approval an overrun asked for, or nobody answered in time. A new model node has no overrun policy, and a policy that a node has practically never fires, because both estimates come from the same request a moment apart. | Send the request again under a new key — the call is estimated anew and takes a hold of its own. To stop such refusals, turn the overrun policy of the model node off and publish a new version. |
overrun_approval_required | 402 | OverrunApprovalRequiredException PHPOverrunApprovalRequiredError Node.js | The estimate of a model call in a flow, made again after its hold was taken and before the provider is called, came out larger than the hold by more than the overrun tolerance of the node, and the overrun policy of the model node asks a person to approve the difference. The execution waits for that decision, so the request is answered with the waiting execution, not with this code. A new model node has no overrun policy, and a policy that a node has practically never fires, because both estimates come from the same request a moment apart. | Decide on the approval in the console. An approval that is refused, or that nobody decides in time, ends the execution with overrun_stopped. |
A policy that refuses is not part of the family. policy_denied and model_not_allowed are answered with 403 and raised as PolicyDeniedException or PolicyDeniedError — Policies says how a policy is checked. A blocked customer is refused the same way, with customer_blocked, as CustomerBlockedException or CustomerBlockedError, which extend the type of a policy refusal.
Two codes of the family, overrun_stopped and overrun_approval_required, come from the overrun policy of a model node in a flow, and such a policy practically never fires — their rows in the table above say why.
Catch it
use Slen\Sdk\Exceptions\OperationUncertainException;
use Slen\Sdk\Exceptions\SlenException;
use Slen\Sdk\Exceptions\SpendRefusedException;
use Slen\Sdk\Modules\Operations\Options\ExecuteOptions;
$input = ['messages' => [['role' => 'user', 'content' => 'Write a short follow-up email.']]];
try {
$execution = $slen->operations()->execute('generate_email', $input, new ExecuteOptions(idempotencyKey: 'order-42:email'));
echo $execution->status->value, PHP_EOL;
} catch (SpendRefusedException $error) {
// The spend was refused, and the code says what refused it.
echo 'refused: ', $error->errorCode, PHP_EOL;
} catch (OperationUncertainException $error) {
// A provider was called and its answer was lost: the call may have been paid for.
echo 'uncertain, request ', $error->requestId, PHP_EOL;
} catch (SlenException $error) {
echo $error->statusCode, ' ', $error->errorCode, ': ', $error->getMessage(), PHP_EOL;
}The code says which limit refused the call, and the table above says what to do about each.
The refused call cost nothing: its provider was not called, and no money stays held for it. In a flow, the model calls of the nodes before it were paid for.
The same request is refused again until something changes — a budget starts its next window, a limit is raised, the customer is topped up.
concurrency_limit_exceededis the exception: it passes once a running call ends.Do not send a refused request again in a loop.
The second form: an execution that ended rejected
A run is answered with the error once. The execution it opened is closed with the status rejected and keeps the code. Its cost.estimated is the estimate of the refused call — spend the refusal prevented, not money spent — or null when the refusal named no estimate, as for a customer out of credits. Whoever reads it later gets the execution and no error: a repeat of the request under its key, a wait, a status read, a run sent in async mode. So a run that you send again after a refusal goes under a new key.
$again = $slen->operations()->execute('generate_email', $input, new ExecuteOptions(idempotencyKey: 'order-42:email'));
if ($again->isRejected()) {
echo 'rejected: ', $again->errorCode, PHP_EOL;
}When a model node has a branch on its Deny port, a refusal does not end the execution. The flow goes on along that branch and decides what the caller hears.
A Reject Request node may end an execution with any code, a code of this family too, and then no error is raised either. Code that reacts to a refusal handles both forms: the catch, and the status of what came back.
Where the limits are
The budgets of a project are on Budgets.
The credits of your customers are on Customers, and your application tops them up with
customers.topUp()— Customers and credits.
Where to go next
Handling errors — every error of the SDK, and whether it cost money.
Error codes — what each code means and what to do about it.
Async runs and waiting — how to read the way an execution ended.
Built from openapi b070e1f, sdk contract 6a2e0e0, php 4089db3, node 0fa87be