# AI in your application

> Have a model read text, PDFs and images and return text or structured data, from your application's code, with no AI account or key. Declare the permission, call ai.generate.

Your application can ask a model to read text, PDFs and images and to answer in text, or in JSON that
follows a schema you give. Apigami calls the model in its own name, in the EU: you need no AI account, no
key and no server. What you send is processed without being stored and is never used for training.

```json title="POST /call"
{
  "op": "ai.generate",
  "params": {
    "instructions": "Extract the person's name and city. Use null for what the text does not say.",
    "input": [{ "text": "Jean Dupont, developer, lives in Lyon." }],
    "output_schema": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "city": { "type": ["string", "null"] }
      },
      "required": ["name", "city"]
    }
  }
}
```

```json title="Result"
{
  "result": {
    "output": { "name": "Jean Dupont", "city": "Lyon" },
    "finish_reason": "stop",
    "usage": { "input_tokens": 188, "output_tokens": 43 }
  }
}
```

`ai.generate` is answered by Apigami itself, never by a browser: it takes no `domain`, no `user_ref`, and
opens no approval window.

The AI is in preview, open to a few teams. Any other team's call answers `403 ai is not open to this group
yet`.

## Before the first call

1. Declare the permission in your manifest, beside your sites
   ([Permissions: ai](/docs/app-manifest/#permissions-ai)):

   ```json
   "x-apigami": { "permissions": ["ai"] }
   ```

   The version whose code calls must declare it: the one a run executes, the production version for your
   `apg_` key and for `APIGAMI_KEY`. A brick's production version must declare it too, since that is what
   the connect page of the applications using it shows.
2. Deploy, and publish to call from your server with your API key. The connect page then lists, among
   what your application may do, « Use an AI: what you entrust to it is processed in the cloud, without
   storage or training ». Nothing to tick.

## Who calls

| Caller | Credential |
|---|---|
| Your server | your API key, `apg_…` |
| The action of a cron, for several people | `env.APIGAMI_KEY`, the production version's key |
| An action, a trigger, a schedule | the run token in `X-Apigami-Run` |

The AI answers your application, not a person: a run with nobody behind it, such as an `open_action` or a
schedule, calls it like any other. A page cannot: its token is the visitor's, who could write the
instructions. Call an action of yours from the page with `apigami.call`, and call the AI from that action.

```ts title="actions/extract.ts"
export const extractPerson = async (env, runToken: string, pdfBase64: string) => {
  const answer = await fetch(`${env.APIGAMI_API_URL}/call`, {
    method: 'POST',
    headers: { 'X-Apigami-Run': runToken, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      op: 'ai.generate',
      params: {
        instructions: 'Extract the person this resume is about.',
        input: [{ file: { media_type: 'application/pdf', data_base64: pdfBase64 } }],
        output_schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
      },
    }),
  })
  const body = await answer.json()
  // Never log the input or the output: your logs are kept, and the content is someone else's.
  if (!answer.ok) throw new Error(`ai refused: ${answer.status} ${body.error}`)
  return body.result.output
}
```

## The parameters

| Param | | |
|---|---|---|
| `instructions` | required | What the model must do, 1 to 32 000 characters |
| `input` | required | 1 to 10 parts, read in order: `{ "text": "…" }` or `{ "file": { "media_type": "…", "data_base64": "…" } }` |
| `output_schema` | optional | A JSON Schema of the answer, 16 kB at most. Absent, the answer is text |
| `max_output_tokens` | optional | 1 to 16 384, 4 096 by default |

The model, its temperature and its settings are Apigami's: they may change, never the contract.

### Documents

- Text parts total 500 000 characters at most.
- A file is a PDF (`application/pdf`), plain text (`text/plain`) or an image (`image/png`, `image/jpeg`,
  `image/webp`), in standard base64. Its first bytes must match its type.
- 14 MB of files at most per call, decoded, all files together.
- A Word or OpenDocument file is refused with a 415: convert it to PDF or text first.

### Structured output

With `output_schema`, `output` is the parsed JSON, which follows the schema. A schema written by zod's
`z.toJSONSchema` or pydantic passes as it is. The keywords the model reads are `type`, `properties`,
`required`, `items`, `prefixItems`, `additionalProperties`, `enum` (strings or numbers), `anyOf`, `oneOf`,
`title`, `description`, `format`, `minItems`, `maxItems`, `minimum`, `maximum`, `propertyOrdering`,
`$schema`, `$id`, `$anchor`, `$defs` and `$ref`. Any other, such as `pattern` or `minLength`, is refused
with its path rather than silently ignored. A value that may be absent is written
`"type": ["string", "null"]`.

`description` is read by the model: say there what a field holds, and what to answer when the document
does not say.

### Usage

`usage` counts the tokens of the call. `output_tokens` includes the model's reasoning. Without a schema,
`finish_reason` is `max_output_tokens` when the text was cut short; with a schema, a cut answer is a 422,
since half a JSON is of no use.

## Refusals

| Status | `error` | What to do |
|---|---|---|
| `400` | `ai answers the caller, not a person: remove user_ref and install_ref` | Send neither |
| `403` | `a page cannot call the ai: …` | Call it from an action, which the page calls with `apigami.call` |
| `403` | `the calling app does not declare this app in its dependencies` | A brick's run, after the application calling it removed it |
| `409` | `publish a version with app_publish before an api key calls the ai: …` | Publish once: your key runs production's manifest |
| `403` | `ai not permitted for this app: …` | Declare `"permissions": ["ai"]` in the version that calls, and deploy |
| `403` | `ai is not open to this group yet` | The AI is in preview: write to contact@apigami.com |
| `400` | `invalid ai params: params.input[1].file.data_base64: does not start like application/pdf` | Fix the field it names |
| `413` | `ai input too large: at most 14 MB of files per call` | Send fewer or smaller files |
| `415` | `unsupported media type …: send pdf, text or an image` | Convert the file to PDF or text |
| `400` | `invalid ai params: the model cannot read this input, …` | Check the files, and the schema |
| `503` | `ai is not configured` | Apigami's side: write to contact@apigami.com |
| `503` | `the model is busy, try again in a minute` | Wait for `Retry-After`, then try again |
| `502` | `the model could not answer, try again later` | Try again later |
| `422` | `the model declined to answer: safety` | The content was refused by the model's filters: do not retry as is |
| `422` | `ai output cut at max_output_tokens: …` | Raise `max_output_tokens`, or ask for less |

A refusal never repeats what you sent. Apigami never retries a call by itself: a call is billed once.

## What is kept

- **Apigami** keeps one line per call: the application, its version, the model, the tokens, the duration
  and whether it answered. Never the instructions, the files nor the answer.
- **The model's provider**, Google Cloud, processes the call in the EU and stores nothing of it, with one
  exception: a request its abuse filters flag may be kept up to 90 days, in the EU, for review. Nothing
  is used for training.
- **Your application** is responsible for what it does with the answers. Never write an input or an
  output of the AI to your logs: they are kept.