Skip to content

AI models ​

The catalogue's model apps put a language model in a workflow step or a rule: an answer to a prompt, as text or as JSON of a schema the step writes, and decisions on named yes-or-no, choice and score questions about a run's data, which later steps route on by their probability and confidence. Five apps reach the vendors tenants use: claude, chatgpt (named "ChatGPT (OpenAI API)" in the console), gemini, jev and openai-compatible. A tenant installs one as often as it needs, each install with a connection, keys, approved hosts and models of its own, and calls it as any app (Apps).

Every request goes through the app runner's host, never from the app's module: the module asks the host's ai function (Host interface), and the host fills in the install's keys, sends the request only to a host the install approved, holds it to its time and checks the model's answer against what was asked, through the Vercel AI SDK in the runner. A key is a secret of the install: it never reaches the module, a run, a log or an answer.

The apps ​

AppConsole nameConnectionsActions
claudeClaudeAnthropic's API, or Amazon Bedrockgenerate, decide
chatgptChatGPT (OpenAI API)OpenAI's API, or an Azure OpenAI resourcegenerate, decide
geminiGeminiThe Gemini APIgenerate, decide
jevJevTypeSafe's APIdecide
openai-compatibleOpenAI-compatibleAny server that speaks OpenAI's Chat Completionsgenerate, decide

Every app answers in the same shape: generate and decide take the same input and give the same output whichever app an install runs, so a step's fallback may name another app's install (below). The apps share their code and one version, 0.1.0 (Releasing). They are free; the vendor bills for its tokens.

Installing a model app ​

An install's configuration holds its connection (how it reaches the models, by type), its model (the model a step asks unless it names another) and, for the apps that answer prompts, the instructions every answer is given before a step's own and the defaults of its answers (generation: maxOutputTokens, 4096 unless the install says otherwise, 1024 for openai-compatible; temperature), and decideModel, the model decisions ask when it is not the install's. allowedModels (20 at most) are the models a step may name besides the install's model (and, for decide, its decision model). The configuration is strict: a field no model app reads is refused when the install is saved.

Connect › Apps › an app › Add install shows a form per connection type: the model field suggests the vendor's current models and links to its list, an OpenAI-compatible server's fields fill in from a known one (OpenRouter, Groq, Mistral, Together AI, Ollama), and the keys the connection needs show at the foot of the form, each set there (Admin console). Through the API the sequence is any install's (Installing a platform app):

bash
VERSION=$(curl -s "$API/apps" -H "authorization: Bearer $TOKEN" |
  jq -r '.items[] | select(.name == "claude" and .publisher == "platform") | .latest.id')
# Created off: its hosts approved (the manifest's), its key to store before it runs.
curl -s -X POST "$API/app-installs" -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"name":"claude-main","versionId":"'"$VERSION"'","enabled":false,
       "config":{"connection":{"type":"anthropic"},"model":"claude-opus-5-5"},
       "approveHosts":["api.anthropic.com"]}'
curl -s -X POST "$API/app-installs/claude-main/secrets/anthropicApiKey" \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"value":"sk-ant-…"}'
curl -s -X PUT "$API/app-installs/claude-main" -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"versionId":"'"$VERSION"'","enabled":true,"config":{"connection":{"type":"anthropic"},"model":"claude-opus-5-5"}}'

Connections ​

typeAppFieldsSecrets
anthropicclaudebaseUrl (a proxy's)anthropicApiKey
bedrockclauderegion, auth (api-key or sigv4), baseUrl (the region's endpoint, or a proxy's)bedrockApiKey; or awsAccessKeyId and awsSecretAccessKey
openaichatgptapi (responses or chat), organization, project, decideEngine, baseUrlopenaiApiKey
azurechatgptresourceName or baseUrl (one of them), apiVersion (v1 or a dated one), apiazureApiKey
geminigeminibaseUrlgeminiApiKey
typesafejevbaseUrltypesafeApiKey
openai-compatibleopenai-compatiblebaseUrl, name, auth (bearer, header with apiKeyHeader, or none), structuredOutputs, headers, queryParamsapiKey, unless auth is none

A connection reaches the vendor's own API unless it gives a baseUrl: the manifest's hosts name the vendors' APIs (api.anthropic.com, api.openai.com, *.openai.azure.com, generativelanguage.googleapis.com, api.typesafe.ai), and a base URL's host is one of the install's hosts, approved when the install is saved (hostsFromConfig: ['/connection/baseUrl']). A Bedrock connection always gives one, the region's endpoint, which the console fills in from the region. SigV4 signs for the connection's region, so an endpoint of another region cannot serve it: the console holds such a save, and an install saved so through the API fails every call with a final error naming the endpoint. A key is required while the connection needs it (requiredWhen): switching the connection asks for the new one's key, and the install stays off until it is stored. A connection's headers never carry a key: authorization, x-api-key, api-key and the like are refused, a header's value is printable ASCII, and a key goes in apiKey.

OpenAI's Responses API is called with storing off. An OpenAI connection chooses how decide answers (decideEngine): native, OpenAI's Decisions API, or adapter, a model's structured output (the default); Azure has no Decisions API. An OpenAI-compatible server answers decisions only with structuredOutputs on, which says it holds a model to a JSON Schema (response_format: json_schema); without it, decide is refused before any request.

Models ​

The model field takes the id the vendor names a model by, an Azure deployment's name, or a Bedrock model id or inference profile; the console suggests current ones:

ConnectionModels
anthropicclaude-opus-5-5 (Anthropic's default), claude-fable-5-1 (the most capable), claude-sonnet-5-5, claude-haiku-4-5 (the fastest) (models)
bedrockanthropic.claude-opus-5-5, global.anthropic.claude-fable-5-1, anthropic.claude-sonnet-5-5, anthropic.claude-haiku-4-5-20251001-v1:0, or an inference profile with its geography (us., eu., global.)
openaigpt-6-sol, gpt-6-luna, gpt-6-astra, gpt-5.5 (models)
azurethe name of one of the resource's deployments
geminigemini-pro-latest, gemini-3.8-flash, gemini-3.5-flash, gemini-3.5-flash-lite (models)
typesafejev-latest
openai-compatiblethe model as the server names it

Claude's current models think before they answer, and their thinking counts against maxOutputTokens: the default of 4096 leaves room for it, and a long answer needs more. Opus 5.5, Fable 5.1 and Sonnet 5.5 take no temperature: leave it empty for them, or their calls fail with the vendor's 400.

Asking for an answer: generate ​

A call_app step or an app rule calls generate with the prompt:

FieldWhat it holds
promptWhat to answer: <<name>> takes a variable's value
instructionsGiven after the install's own: who the model is, how it answers
variablesThe values of <<name>>, fixed or from the run (the input mapping's variables.<name>)
outputHow the answer is shaped: text (the default), object with a JSON schema, array of an element schema, choice of options, or json
modelThe install's model or one it allows; the install's when empty
maxOutputTokensThe install's when empty, 32768 at most
temperature0 to 2; the install's when empty

Run data reaches only the variables (mappable: ['/variables']): the prompt and the instructions are the step's own, and a variable's value is put in once, as text or as JSON, never read again, so run data holding <<other>> stays that text. A name no variable holds is a final error. An object answer's schema holds the model to it: every property is required, none other is allowed (additionalProperties: false), a field that may be missing is nullable ("type": ["string", "null"]), and $ref, pattern, oneOf, allOf, not and if are refused, at most 32 KiB, 10 levels deep and 500 properties; the console checks the schema as it is written, and the host checks the model's answer against it.

The answer is text (a text answer's) or output (any other), with finishReason (stop, length when it ran out of tokens, content-filter when the vendor held it back), usage (the input and output tokens), the model the vendor says answered and the provider (the connection type). Later steps read it under the step's output key, an object answer's fields too (summary.output.verdict). It takes two minutes at most, is tried twice, and answers 900 KiB at most.

Asking for decisions: decide ​

decide asks named questions about a state, up to 32:

  • yes or no (boolean): what to decide, optional criteria for yes and for no, and the threshold (0.5 unless the step says otherwise) from which the answer's value is yes;
  • one of the options (choice): 2 to 255 options, each a name with an optional description;
  • a score (score): 2 to 10 levels, from the lowest.

A question's id and an option's name are a letter, then letters, digits, _ or -: they are segments of the paths that read the answers (ai.answers.fraud.probability). The state is what the questions are about: the step's variables as one JSON object (when there is neither of the others), a text the step writes (stateTemplate, its <<name>> filled), or a state as it comes, text, a JSON object, or text and JSON parts, the run's data through the input mapping (state ← a data path) or the step's own. A state is never filled: what the run data holds, <<…>> included, reaches the model as it is. Run data reaches only state and variables.

Each answer, under answers.<id>:

FieldWhat it holds
valueWhat an app rule compares: yes or no against the threshold, the option chosen, or the score rounded to its level
probabilityP(yes) for a yes-or-no; the chosen option's or the level's when the model gave a distribution; null when it gave none
probabilitiesThe distribution over the options or the levels, when the model gave one
confidenceHow sure the answer is: the vendor's own when it gives one, else the likeliest outcome's probability; absent without either
choice, scoreThe option chosen; the score on the levels, fractional

Jev answers natively, with each outcome's probability and a confidence of its own; ChatGPT does too through OpenAI's Decisions API (decideEngine: 'native'). The others answer through a model's structured output (method: 'adapter'): a yes-or-no's probability is the model's own estimate, and a choice or a score comes without a distribution, so its probability is null and it has no confidence. A decision asks the install's decision model (decideModel), else its model, unless the step names the install's model or one it allows. decide takes 30 seconds at most and is tried twice; the answer also carries usage, model, provider and method.

Routing on an answer ​

A branch after a decide step routes on its answers' paths, which the step's editor and the branch's path picker offer by the questions the step asks. Ask the question whose yes lets the run go on (legit: is the request legitimate?), go on when ai.answers.legit.probability is at least 0.8, and let the branch's default create a case: a model unsure, or a choice or a score answered without a distribution (probability null), goes to a person rather than through. An app rule compares one value instead: resultPath: answers.fraud.value with failWhen: true fails the rule on a yes; the rule's result picker offers the paths (Rules).

json
{
  "id": "route",
  "type": "branch",
  "conditions": [
    {
      "when": { "path": "ai.answers.legit.probability", "op": "gte", "value": 0.8 },
      "next": "approve"
    }
  ],
  "default": "review"
}

Falling back to another install ​

Every model app takes the same input and answers in the same shape, so a step's or a rule's fallback may name another app's install: a decide on jev-main falls back on claude-main's decide when Jev is down, its questions and its state the same (Copy the input above puts the step's input in the fallback's, less a model its install does not allow). The fallback answers when the install's circuit is open or its vendor's outage outlasted the two attempts (Apps: reference); an attempt the step's time cut short is no reason to fall back.

Time, retries and failures ​

A call_app step's activity holds a call for two and a half minutes: generate's two minutes, then the time to answer and record it. Each attempt runs for the action's time cut to what the step has left, and a retry starts only when a whole attempt still fits, so no attempt outlives the step and a vendor is never asked twice for one attempt. An app rule's calls share the evaluation's minute: saving a rule whose action, or its fallback's, may take more than 50 seconds warns (rule_action_timeout).

A vendor's 408, 409, 429 and 5xx, a failed connection and a request that ran out of its time are worth another attempt: the action's second waits the policy's backoff or the vendor's Retry-After, whichever is longer, and the step's own retries wait no less. An exhausted quota (insufficient_quota), any other 4xx, a refusal by the model, an answer outside the schema, and anything the connection cannot do (decisions on a server without structured outputs) are final. The vendor's outages count against the install's circuit; an attempt the step's time cut short never does.

Usage and cost ​

Every answer carries the tokens its model used, and every invocation records them (usage.ai: the requests sent to the vendor, the input and output tokens), a call that failed after its model answered included; the runner counts them on aletheia.app.ai.requests, aletheia.app.ai.tokens and aletheia.app.ai.duration (Operations). The vendors bill per token, at their own prices: the platform holds no token budget, while an install's budgets and the operator's quotas count its calls (Apps: reference).

Data protection ​

A step sends the vendor of the install it calls the install's instructions and its own, its prompt or its questions (their texts filled with the variables), the variables, the state, and the shape it asks the answer in (a schema or options), and nothing else: no document or image reaches a model. That vendor is the one the install's administrator chose and approved the host of; the vendor's own terms say what it keeps and for how long (OpenAI's Responses API is called with storing off; Anthropic, Google, Microsoft and AWS each have their own retention and zero-retention terms). Run data that names a person reaches the vendor when a step maps it there: map only what the question needs. The runner's logs carry no prompt, state or answer (a debug line per request, without content), and an answer never carries a key; the run's context and the invocation keep the step's input and the answer as any app's call does, for as long as the platform keeps runs and their records.

Prompt injection ​

Run data in a prompt is data the model reads, and an applicant may write instructions into it. The apps keep the step's own text apart: run data fills only variables and the state, once, and a mapped state is never filled; a decision's answers are held to their questions' options and levels, and an object answer to its schema. Route on probability and confidence with a person behind the default branch, ask for the facts a rule then weighs rather than for the decision itself, and never let a model's answer alone approve what a person should see.

What they do not do ​

Models read text and JSON only: documents and images, streaming answers and tool calling wait (Roadmap). There is no token budget, no Vertex AI (it needs Google's OAuth), no Azure Entra ID and no Bedrock STS role, no call of more than two minutes, and no retention period or erasure of a model's inputs and answers of their own. A step's or a rule's input written through the API is checked when it runs, not when its definition is saved.

Released under the Apache-2.0 License.