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.
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.
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 | 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 | No |
chat: one call
ctx.ai.openai.chat(params) makes one call and returns the complete result.
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 |
tools / toolChoice | Tool definitions, see a single tool call |
extra | Escape hatch merged into the request body, see escape hatches |
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.
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 }
}
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.
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 |
A second round is yours to run: append the tool result to messages and call again.
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.
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? }.
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:
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:
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():
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, Text to speech, Func backend capabilities.
