Skip to content

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 ai step 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.

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.

Terminal window
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 }
}
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.

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.

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.

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.

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.

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.