---
title: App AI
description: "Text, image understanding, tools, and thinking for your published apps."
category: sdk
order: 2
agent: "Use @coduck/sdk/ai server-side after Cloud publication. ai.chat, ai.stream, ai.run, ai.usage. Read ai.usage() for the current per-plan allowance and rollout access; app-AI credits are independent of build credits. Read app_ai skill for exact API, authorization, tool, and retry rules."
---

# App AI

Use CoDuck AI in your published app without a separate model-provider account. The monthly app-AI allowance depends on the billing owner’s plan and rollout access. Studio defaults to 25,000 credits; use `ai.usage()` for the current allowance, shared across the owner’s projects and visitors. App-building credits are separate.

SDK 0.2.0 adds `@coduck/sdk/ai`. CoDuck installs the SDK and injects the project API key when publishing. Run AI in server routes after authenticating the visitor. Never expose `CODUCK_API_KEY` in browser code.

## Text and thinking

```ts
import { ai } from '@coduck/sdk/ai';

// Inside your authenticated server handler:
const response = await ai.chat({
  messages: [{ role: 'user', content: 'Summarize this feedback: the checkout was easy.' }],
  user: authenticatedUserId,
  max_tokens: 512,
  thinking: false,
}, { idempotencyKey: requestId });

const text = response.choices[0].message.content;
const credits = response.credits.used;
```

Use one request ID per logical request and reuse it only for a retry with identical content. Thinking is bounded and included in output tokens. Responses may contain `message.reasoning` when thinking is enabled; it is not an extra charge.

## Streaming

```ts
for await (const event of ai.stream(input, { idempotencyKey: requestId })) {
  if (event.type === 'text') writeToBrowser(event.delta);
  if (event.type === 'complete') saveUsage(event.response.credits.used);
}
```

Events are `started`, `text`, `thinking`, `tool_call`, and `complete`. Only `complete` confirms final usage. The API keeps recording usage if the browser disconnects.

## Image understanding

Use a content array with text and a PNG, JPEG, or WebP data URL:

```ts
messages: [{ role: 'user', content: [
  { type: 'text', text: 'Describe this product photo.' },
  { type: 'image_url', image_url: { url: imageDataUrl } },
]}]
```

Up to two images, each data URL at most 1.5 million characters, and a total request body at most 3 MB. Resize large uploads. Remote image URLs and image generation are not supported.

## Tools

`ai.run()` manages a bounded server-side tool loop. Define JSON schemas, validate received arguments, and check visitor permissions inside each handler. Tool results are sent back to the model; every model round is metered.

```ts
const result = await ai.run({
  messages: [{ role: 'user', content: 'What is 7 plus 9?' }],
  maxSteps: 4,
  tools: {
    add: {
      description: 'Add two numbers',
      parameters: {
        type: 'object',
        properties: { a: { type: 'number' }, b: { type: 'number' } },
        required: ['a', 'b'],
        additionalProperties: false,
      },
      execute: (args) => {
        const { a, b } = args as { a: unknown; b: unknown };
        if (typeof a !== 'number' || typeof b !== 'number') throw new Error('Invalid numbers');
        return { sum: a + b };
      },
    },
  },
}, { idempotencyKey: requestId });
```

For side effects, deduplicate the handler’s `toolCallId` before acting. The model cannot authorize actions. Maximum eight steps and 16,000 characters per tool result. Reaching the step limit stops the loop; completed model calls still consume credits.

## Usage and errors

`await ai.usage()` returns the shared allowance, used and reserved credits, reset date, and this project’s recent requests. The project key cannot reveal other project names. Cloud → AI shows the same ledger, and its test prompt uses real credits.

Credits = input tokens ÷ 10,000 + output tokens ÷ 1,000. A request reserves its maximum possible usage, then settles the actual amount. Reasoning is counted once in output. There is no rounding up to whole credits.

| Response | Meaning |
|---|---|
| 409 `cloud_publish_required` | Publish this project first |
| 402 `app_ai_plan_required` | An active paid plan with an AI credit allowance is required |
| 402 `app_ai_limit` | Insufficient credits for the maximum reservation |
| 429 `ai_busy` / `ai_rate_limit` | Retry the same request ID after `Retry-After` |
| 409 `request_in_progress` / `request_already_processed` | The request was already admitted; inspect its status |
| 502 `usage_unconfirmed` | Final usage was lost; the reservation is accounted for and labeled estimated |

Do not silently mint a new ID after uncertain failures. Successful retry responses are cached, encrypted, for 24 hours. The idempotency record remains after that response expires. Raw prompts are not stored in the gateway usage ledger. Keep the rest of your app usable when AI is unavailable.

The agent’s `app_ai` tool and [`coduck ai`](/docs/cli/commands#app-ai) commands use the same gateway and credits.

## Gradual availability

App AI also requires the billing account to be enabled in the rollout. `ai.usage()` returns `rollout: { enabled, status }`, where status is `enabled`, `not_in_rollout`, `paused`, or `unavailable`. The top-level `enabled` combines the service switch and rollout; `eligible` remains the separate plan entitlement. `403 app_ai_not_in_rollout` and `503 app_ai_paused` reject before reservation, so they consume no credits. The same rule applies to chat, streaming, tool rounds, API, CLI and agent calls. Existing cached retries are also unavailable while access is paused or removed. In-flight accepted requests finish and settle normally.

CoDuck administrators use **Admin → Settings → App AI rollout**. Each paid plan has its own percentage and monthly AI credit allowance. For example, Studio at 100% and Plus at 50% selects all active Studio subscribers and about half of active Plus subscribers, with each account receiving its own plan’s configured allowance. Zero percent is invite-only; zero credits disables that plan. Add a billing-owner email with **Add account**, review the visible list, then **Save AI rollout**. Remove edits the draft list; save applies it. Removing a manual override does not exclude an account selected by its plan percentage. Percentages select approximately that share of accounts within the plan, not requests or concurrent users. All projects and visitors of the same payer share a stable assignment; increasing the percentage preserves earlier selections. Manual access bypasses percentages but still requires an active paid plan with credits, Cloud publication and request limits. The current billing-owner plan comes from the database, never client context. Plan changes take effect on the next admission.

The API installs `@coduckai/flags@0.1.0` and evaluates locally with a stable account-ID salt. A versioned `PlatformSetting` row in each environment stores the rollout. Admission reads that row inside the existing credit transaction and shares its lock with admin writes. Once a save completes, subsequent admissions see it without redeploying; the Cloud display refreshes within 15 seconds or on Refresh. There is no separate flag server or provider credential in the browser. Missing configuration defaults to paused at 0%; malformed/unreadable configuration fails closed. Admin writes require the existing administrator role and current revision, and create an audit event. Existing global-percentage configurations migrate on read to Studio only, retaining the same hash salt and cohort. Pro and Plus stay at zero. New saves persist all three plans; no schema migration is needed. The admin lookup endpoint `/api/admin/app-ai/accounts?email=…` resolves one exact email and requires the administrator role.

Runtime requirement: API build and runtime require Node 22.13+ for CoDuck Flags; `.nvmrc` and CI select Node 22. Before production release, provision that supported runtime for the API process. The PR uses a slot-local Node runtime; the host default is not changed. Reapply the process interpreter after a PR rebuild, alongside the private model configuration.
