# AI providers

> Each tenant's own provider keys as credential references, and a registry of provider batch jobs with a poll schedule.

Source: https://bettersupabase.com/docs/blocks/ai-providers

The `ai-providers` block holds what an app tracks about the model providers
it calls on a tenant's behalf: the tenant's own API keys (bring your own
key) and batch jobs that run at the provider for hours. Code sandboxes live
in the [ai-chat](/docs/blocks/ai-chat#sandboxes) block. It stores
no secret and imports no SDK; the
[AI SDK helpers](/docs/ai-sdk/providers) and [batches](/docs/ai-sdk/batches)
connect it to the AI SDK.

```bash
better-supabase sql add ai-providers   # adds tenant and access as well
```

| Table              | Holds                                                                               |
| ------------------ | ----------------------------------------------------------------------------------- |
| `ai_provider_keys` | One key per tenant, provider and name: a `credential_ref`, `settings` and `enabled` |
| `ai_batches`       | A provider batch: its reference, status, counts, expiry and when to poll it next    |
| `ai_batch_items`   | One result per request of a finished batch: status, output, usage and error         |

Organization exports leave out `credential_ref`, and the organization
purge in [data lifecycle](/docs/blocks/data-lifecycle#credentials) revokes each key's credential before it deletes the row.

| Permission  | Lets a member                                        | Default roles              |
| ----------- | ---------------------------------------------------- | -------------------------- |
| `ai.create` | start batches, and read their own batches            | `owner`, `admin`, `member` |
| `ai.admin`  | list, save and remove the keys, and read every batch | `owner`, `admin`           |

| Option      | Default | Sets                                                                 |
| ----------- | ------- | -------------------------------------------------------------------- |
| `pollEvery` | 60      | Seconds between two polls of a batch; the first poll waits this long |

## Creating the client [#creating-the-client]

```ts title="lib/ai-providers.ts"
import "server-only";

import {
  createAiProviders,
  rpcTransport,
} from "better-supabase/blocks/ai-providers";
import { vaultCredentials } from "better-supabase/credentials";

const service = rpcTransport(bs.admin().$client, { schema: "api" });

export const providersFor = (supabase: SupabaseClient) =>
  createAiProviders({
    transport: rpcTransport(supabase, { schema: "api" }),
    service,
    credentials: vaultCredentials({ transport: service, schema: "api" }),
    schema: "api",
  });
```

## Keys [#keys]

A key row names a credential, never the key itself. Store the key with the
[credential provider](/docs/extending/credentials) first, then save the
reference:

```ts
import { tenantCredentialRef } from "better-supabase/credentials";

const ref = tenantCredentialRef(organizationId, {
  provider: "vault",
  secret: "anthropic",
});
await credentials.set(ref, apiKey).orThrow();
await providers.keys
  .save(organizationId, { provider: "anthropic", credentialRef: ref })
  .orThrow();
```

The ref must carry the key's tenant
([tenant refs](/docs/extending/credentials#tenant-refs)); any other ref fails
with `CREDENTIAL_REF_FOREIGN`. `save` replaces the key with the same provider and name, and revokes the
credential the old row named. `remove(keyId)` revokes the credential and
deletes the row. `resolve(organizationId)` reads the enabled keys with their
tokens for one request, through the service role; pass the result to
[`byokOptions`](/docs/ai-sdk/providers). Members never see a token: `list`
returns the rows without one.

When a tenant is deleted, run `keys.removeAll(organizationId)` before the
tenant's rows go, so every credential is revoked at the provider.

## Batches [#batches]

`batches.record` stores a batch the app started, with the SDK's reference.
The poll job claims due batches with `due({ batch, leaseSeconds })`, so two
workers never poll the same one, and writes the outcome with `update` and
`saveItems`. `items(batchId, { cursor, limit })` pages the results by request
id. [`aiBatches`](/docs/ai-sdk/batches) does all of this for AI SDK batches.