Calling the model
Shipped
Your instance already has an AI model configured and paid for. This page is how your own code asks it a question.
There are two ways in, and they are the same model and the same allowance:
POST /v1/ai/complete— one prompt in, one answer out. Called from an app’s own screen under the session the person is already signed in with, or from a program with an access token.- an
aistep in a process — the engine makes the call for you, mid-chain, and hands the answer to the next step. See Writing processes.
A step’s own build function still cannot reach the model, and that is
deliberate: an extension is pure — no secrets, no network — so the call is made
by the engine around it rather than from inside the sandbox.
POST /v1/ai/complete
Section titled “POST /v1/ai/complete”It is a passthrough. No skills, no knowledge-base grounding and no system prompt of ours is added to what you send — the prompt that reaches the model is the prompt you wrote.
curl -X POST https://acme.turbobase.app/v1/ai/complete \ -H 'authorization: Bearer tb_pat_…' \ -H 'content-type: application/json' \ -d '{ "app": "reorder-helper", "prompt": "Summarise this purchase order in one sentence: …", "tier": "fast", "maxTokens": 500 }'{ "text": "A 240-unit restock of three SKUs from Globex, due 14 October.", "model": "…", "tier": "fast", "usage": { "inputTokens": 212, "outputTokens": 19 }}The fields
Section titled “The fields”| Field | Required | What it does |
|---|---|---|
app |
yes | The slug of the app spending this call. The allowance is per app and the usage row is keyed on it, so a call that names no app is one nobody can attribute, bound or bill. It must be an app deployed on your instance; a preview’s slug spends its own app’s allowance. |
prompt |
yes | What you are asking. Non-empty, and at most 100,000 characters. |
tier |
no | Which class of model to use. Defaults to fast; your instance decides which tiers are allowed. |
maxTokens |
no | The most the answer may be. Defaults to 1,024 and is capped by your instance’s per-call limit. |
There is no model field, and sending one does nothing. A caller naming a
raw model id picks the instance’s bill; the tier is the knob that keeps that
decision the instance’s.
From an app, use the client rather than the route
Section titled “From an app, use the client rather than the route”src/lib/turbobase.ts — the file turbobase apps init writes into every app
project — carries askModel, which fills in your app’s slug and turns a limit
refusal into a typed error:
import { askModel, TurbobaseAiLimitError } from './lib/turbobase';
try { const { text } = await askModel({ prompt: 'Summarise this order: …', tier: 'fast' });} catch (err) { if (err instanceof TurbobaseAiLimitError) { // `err.retryAfterMs` is null when waiting would not help — a prompt over // the per-call ceiling, or a tier this instance does not allow. }}An app project scaffolded before this release does not have it; re-copy the
file, or call the route directly and send app yourself.
Who may call it
Section titled “Who may call it”From an app’s own screen, the session cookie the person is already signed in with. Nothing extra to arrange — it is that app’s user asking, under the allowance the instance set for that app.
From a program, a personal access token carrying Ask this instance’s AI
model (ai:invoke). Tick it when you mint the token, on the Access tokens
page — your account menu, or /settings/tokens directly. A token belongs to a
Turbobase account, so that page is not offered to administrators who opened the
app from inside NetSuite.
That permission is not on any token by default, including every token that already exists — nothing gains the ability to spend your model budget by upgrading. It is revocable on its own, without touching what else the token does, and it reaches no rows: a token carrying it and nothing else can neither read nor change your data.
A token without it is refused 403, and the model is not called.
The limits, and what they answer
Section titled “The limits, and what they answer”Every call bills model tokens against your instance, so four ceilings bound it. An administrator sets them; these are the shipped defaults:
| Limit | Default | What it stops |
|---|---|---|
| Tokens per minute | 60,000 | A loop spending a day’s allowance in a minute. |
| Max tokens per call | 8,000 | One prompt taking most of a minute’s allowance. |
| Tokens per day | 1,000,000 | A steady leak nobody notices. |
| Allowed tiers | fast, chat |
An app reaching for a model class this instance does not pay for. |
These apply to each app. Every call must also fit one window the whole instance shares — four times the per-minute and per-day numbers above — so the total stays bounded however many apps are calling at once.
The limits are checked before the model is asked, so a refused call costs nothing. A call that goes through spends what it actually spent, not what it estimated.
The refusals
Section titled “The refusals”| Status | error |
What happened |
|---|---|---|
429 |
ai_token_limit |
A per-minute or per-day ceiling. The body carries retryAfterMs — when it clears — and a retry-after header beside it, so you wait rather than poll. scope says whether it was your app’s own window (app) or the one every app shares (instance). |
404 |
ai_app_not_found |
app is not an app deployed on this instance. |
403 |
view_as_session |
The call came from a view-as session, which is read-only. |
400 |
ai_tier_not_allowed |
You asked for a tier this instance does not allow. Waiting never clears this; ask an administrator. |
503 |
ai_limits_unreadable |
The limits could not be read. Fails closed — the call is refused rather than let through unbounded. |
503 |
llm_unavailable |
No model is configured on this instance. |
503 |
ai_budget_paused |
The instance’s spend gate has paused model calls. The body carries until. |
403 |
insufficient_scope |
The token does not carry ai:invoke. |
retryAfterMs matters more than it looks: without it the only recovery from a
rate limit is to poll, which spends the allowance you are waiting for.
Declaring your app’s own, lower limits
Section titled “Declaring your app’s own, lower limits”The instance’s limits bound the bill. Your app’s own limits bound your
app — so that a runaway loop in one app exhausts its share rather than
everybody’s. Declare them in public/turbobase.json:
{ "name": "Reorder helper", "ai": { "tokensPerMinute": 5000, "maxTokensPerCall": 2000, "tokensPerDay": 50000, "tiers": ["fast"] }}Every field is optional, and each is a narrowing: declare less than the
instance allows and the lower number is what is enforced for your app. Declare
more and turbobase ext deploy / turbobase apps deploy refuses the deploy,
naming the field and both numbers:
this app declares ai.tokensPerMinute of 90000, and this instance allows 60000.The refusal is at deploy rather than at run time on purpose: a declaration above the instance’s limit means an author believes their app may spend more than it can, and the first symptom would otherwise be an unexpected refusal in code they thought they had sized. A deploy is the moment somebody is watching.
What every call records
Section titled “What every call records”One llm_usage row per call, under app:<slug>, with the input and output
token counts, the provider, the model and who asked. That is what the instance’s
own spend reporting reads, and it is why app is required rather than inferred.
A call made by a process’s ai step is recorded the same way, under
process:<id>, and also lands on that run’s ledger with its token counts — so a
re-delivered run hands on the answer it already paid for instead of billing a
second one.