# Call OpenAI models with an integration | Creght

> Use the OpenAI integration on Creght: ctx.ai.openai.chat for translation and conversation, json: true for reliable JSON, tools for a single tool call, image for pictures stored at a permanent URL, and the key never enters the sandbox.

Overview

- [Creght API for AI](/api.md)

Discoverability

- [How to optimize llms.txt](/api/optimize-llms-txt.md)

Site configuration

- [Configure talizen.config.ts](/api/talizen-config.md)
- [Implement domain-based locale routing](/api/domain-locale-routing.md)

Backend

- [Calling external APIs on the server and managing the cache](/api/ssr-external-api-cache.md)
- [Build site backend workflows with Func](/api/func-backend.md)
- [JSON tables: definition, reads, and queries](/api/func-json-tables.md)
- [Uploads: signed direct upload and Func-generated files](/api/func-assets-upload.md)
- [Timeouts and streaming responses](/api/func-timeout-streaming.md)
- [Integrate Alipay PC Web Payment with Func](/api/func-alipay-payment.md)

Integrations

- [Send Email and Verification Codes with Integrations](/api/func-email-integration.md)
- [Take Alipay payments with an integration](/api/func-alipay-integration.md)
- [Take Stripe payments with an integration](/api/func-stripe-integration.md)
- [Call OpenAI models with an integration](/api/func-ai-integration.md)
- [Add text to speech with an integration](/api/func-tts-integration.md)

Auth

- [Require a Verified Email to Sign Up](/api/auth-verified-registration.md)
- [Reset and Change Passwords](/api/auth-password-reset.md)
- [Sign In From a Func](/api/auth-func-login.md)
- [Query users from a Func](/api/func-user-directory.md)

On this page

- [Connect OpenAI](#connect)
- [chat: one call](#chat)
- [Ask for JSON with json: true](#json)
- [A single tool call](#tools)
- [Generate images](#image)
- [Escape hatches: extra and call](#extra)
- [Several channels: via(tag)](#via)
- [Reading the errors](#errors)
- [What this does not do](#scope)

Integrations/Call OpenAI models with an integration

# Call OpenAI models with an integration

Connect the OpenAI integration and use ctx.ai.openai in Func: chat for conversation and translation, json for reliable structured output, tools for a single tool call, image for generated images stored in your own project space. The API key stays on the server.

Copy Markdown link

Connect the OpenAI integration and Func gets `ctx.ai.openai`: chat, translate, extract structured data, make a single tool call, generate images. The API key stays on the server and never appears in your code.

**Scope**

Single calls. The platform does not run a multi-turn agent loop and does not stream, for the reasons in [the last section](#scope).

The shape is OpenAI's `/chat/completions`. Point the integration's base URL at any compatible gateway (self-hosted, an aggregator, a cloud vendor) and your site code does not change at all.

## Connect OpenAI

Open **Backend → Integrations** in the editor, pick **OpenAI**, and fill in the form below. Saving makes a real call upstream, so a bad key fails right there instead of the first time a visitor uses the feature.

| Field | What goes in | Required |
| --- | --- | --- |
| API Key | The `sk-` key, created in the [OpenAI dashboard](https://platform.openai.com/api-keys) | Yes |
| Default model | Used when a call omits one, e.g. `gpt-4o-mini` | Recommended |
| Base URL | Blank means `https://api.openai.com/v1`. Point it elsewhere to switch provider | No |
| Channel tags | A project can hold several configurations; tags tell them apart, see [channels](#via) | No |

**How to write the base URL**

Stop at `/v1`. A trailing slash and a trailing `/chat/completions` are both stripped automatically. Typical values: `https://api.openai.com/v1`, `https://your-gateway.com/v1`.

## chat: one call

`ctx.ai.openai.chat(params)` makes one call and returns the complete result.

```ts
export function translate(input, ctx) {
  const r = ctx.ai.openai.chat({
    messages: [
      { role: 'system', content: 'Translate the user input into English. Output only the translation.' },
      { role: 'user', content: input.text },
    ],
    model: 'gpt-4o-mini',   // omit to use the integration's default
    temperature: 0.2,
    maxTokens: 512,
  })
  return { text: r.content, tokens: r.totalTokens }
}
```

| Parameter | Notes |
| --- | --- |
| `messages` | Required. An array of `{ role, content }`, where `role` is `system` / `user` / `assistant` / `developer` / `tool` |
| `model` | Omit to use the integration's default |
| `temperature` | Not sent at all when omitted |
| `maxTokens` | Not sent at all when omitted |
| `json` | `true` strips the code fence and parses into `data`, see [asking for JSON](#json) |
| `tools` / `toolChoice` | Tool definitions, see [a single tool call](#tools) |
| `extra` | Escape hatch merged into the request body, see [escape hatches](#extra) |

The result:

| Field | Notes |
| --- | --- |
| `content` | The reply text. Empty when the model only asked for a tool, which is not an error |
| `model` | The model that actually answered |
| `finishReason` | `stop`, `length`, `tool_calls` and so on |
| `promptTokens` / `completionTokens` / `totalTokens` | Usage, for your own accounting |
| `data` | Present only with `json: true` |
| `toolCalls` | Present only when the model asked for a tool |

## Ask for JSON with json: true

Getting JSON out of a model is the most common use of this capability: translation, extraction and classification all end there. With `json: true` the platform strips the code fence models love to add and parses what is left into `data`.

```ts
export function extract(input, ctx) {
  const r = ctx.ai.openai.chat({
    messages: [{ role: 'user', content: `Extract the name and city as JSON: ${input.text}` }],
    json: true,
  })
  return { name: r.data.name, city: r.data.city }
}
```

**Why this one step is worth centralising**

Whether a model wraps its answer in a code fence depends on the model and on the day. Writing `JSON.parse` yourself and forgetting to strip the fence fails **intermittently**: fine today, broken tomorrow, and very hard to track down.

`json: true` only controls parsing on our side. It does **not** add `response_format` to the request, because gateways disagree about that field and one unknown field is a 400. Telling the model to emit JSON is still the prompt's job.

## A single tool call

Pass `tools` and `toolCalls` comes back when the model wants one. The typical use is a single round: turn the user's sentence into one structured function call, run it yourself, then answer.

```ts
export function assistant(input, ctx) {
  const r = ctx.ai.openai.chat({
    messages: [{ role: 'user', content: input.text }],
    tools: [{
      type: 'function',
      function: {
        name: 'reschedule',
        description: 'Move a meeting to another date',
        parameters: {
          type: 'object',
          properties: { meetingId: { type: 'string' }, date: { type: 'string' } },
          required: ['meetingId', 'date'],
        },
      },
    }],
    toolChoice: 'auto',
  })

  if (r.toolCalls?.length) {
    const c = r.toolCalls[0]
    // c.arguments is already an object
    ctx.db.update('meetings', c.arguments.meetingId, { date: c.arguments.date })
    return { done: true }
  }
  return { reply: r.content }
}
```

| `toolCalls[i]` | Notes |
| --- | --- |
| `id` | The id of this call. Pass it back if you run a second round |
| `name` | The function the model wants |
| `arguments` | **Already parsed into an object.** `undefined` when the model emitted broken JSON |
| `argumentsRaw` | The raw text, always present |

**Why the platform parses arguments for you**

In OpenAI's raw response `arguments` is a **JSON string**, not an object. Forgetting `JSON.parse` does not raise anything: `args.city` quietly becomes `undefined` and the code carries that undefined onwards. Silent mistakes like this one are exactly what an integration is for.

A second round is yours to run: append the tool result to `messages` and call again.

```ts
const r2 = ctx.ai.openai.chat({
  messages: [
    { role: 'user', content: input.text },
    { role: 'assistant', toolCalls: r.toolCalls },              // replay what the model asked for
    { role: 'tool', toolCallId: r.toolCalls[0].id, content: JSON.stringify(result) },
  ],
})
```

The platform does not run that loop for you. When to stop, whether a tool may read the database, how the timeout is spent: only your site knows.

## Generate images

`ctx.ai.openai.image(params)`. **Use `upload: true`** and the image is stored in your project's own space, with a permanent URL back.

```ts
export const config = { timeoutMs: 120000 }   // image generation takes tens of seconds

export function cover(input, ctx) {
  const r = ctx.ai.openai.image({
    prompt: input.prompt,
    size: '1024x1024',
    quality: 'high',
    upload: true,
  })
  return { url: r.images[0].fileUrl }
}
```

| Parameter | Notes |
| --- | --- |
| `prompt` | Required. A bare string works too: `image('a red fox in the snow, watercolor')` |
| `model` | Defaults to `gpt-image-1`. It does **not** reuse the chat model, those are different things |
| `size` | e.g. `1024x1024`, `1536x1024` |
| `quality` | `low` / `medium` / `high`; dall-e-3 takes `standard` / `hd` |
| `style` | dall-e-3 only: `vivid` / `natural` |
| `background` | gpt-image-1 only: `transparent` / `opaque` / `auto` |
| `n` | How many images, up to 4 |
| `upload` | `true` stores them and returns `fileUrl`; otherwise you get `imageBase64` |
| `filename` | Only used with `upload`. Defaults to a content hash |

Returns `{ model, images: [...] }`, where each image is `{ fileUrl | imageBase64, mimeType, bytes, revisedPrompt? }`.

**Why images should go through the integration**

The image URL OpenAI returns directly is **temporary and expires after about an hour**. Call the API yourself, store that URL in a database, render it on a page, and everything looks right in testing while production slowly fills up with 404s. Nothing raises an error and nothing shows up in the logs.

The platform always asks upstream for image **bytes** rather than a URL, so an expiring address never reaches your code: you get either a permanent link in your own space or base64.

## Escape hatches: extra and call

For fields the platform does not expose, use `extra`. It is merged into the request body as-is and cannot break `messages`:

```ts
ctx.ai.openai.chat({
  messages,
  extra: { top_p: 0.9, seed: 42, response_format: { type: 'json_object' } },
})
```

For endpoints other than `/chat/completions`, use `call(path, body)`. The body goes up as-is, the upstream JSON comes back as-is, and the key still never enters the sandbox:

```ts
const r = ctx.ai.openai.call('/embeddings', {
  model: 'text-embedding-3-small',
  input: input.text,
})
const vector = r.data[0].embedding
```

## Several channels: via(tag)

A project can hold several OpenAI configurations, say a cheap model for classification and a strong one for writing, or one official endpoint and one self-hosted gateway. Tag each configuration and pick one with `via()`:

```ts
ctx.ai.openai.via('cheap').chat({ messages })
ctx.ai.openai.via('strong').chat({ messages })
```

Without `via()` the configuration tagged `default` is used.

## Reading the errors

| Error | What to fix |
| --- | --- |
| `ai integration has no api key` | The integration is not connected, or the key was cleared |
| `model is required` | No `model` in the call and no default on the integration |
| `message at index N has empty content` | One message has empty `content` |
| `message at index N has role tool but no toolCallId` | A second round without `toolCallId` |
| `ai response is not valid json` | `json: true` but the model did not return JSON. The error carries a preview, so you can tell a vague prompt from a model that went off |
| `the image provider returned a temporary url` | The gateway ignored the request for image bytes. Use `call()` and handle it yourself, or use a gateway that supports `b64_json` |
| `invalid n` | At most 4 images per call |

Upstream errors (out of quota, unknown model, rate limited) keep their message but come back as 400 or 502. An upstream 401 is never passed through as-is, since that would make the page think the visitor is signed out.

## What this does not do

| Not included | Why |
| --- | --- |
| Multi-turn agent loops | The stopping rule, the round budget and tool permissions are yours. A single tool call is supported; the loop is your code |
| Streaming | A Func returns one value. For a typewriter effect, push it yourself with `ctx.sse` |
| Caching and deduplication | Only your site knows what counts as the same request. Keep your own table |
| Platform credits | This is your own API key and your own bill; the platform stays out of it |

See also: [More integrations](/en/docs/ai/more-integrations.md), [Text to speech](/api/func-tts-integration.md), [Func backend capabilities](/api/func-backend.md).

> Full page index: [/llms.txt](/llms.txt)
