Call AI models

playcode.ai calls the AI models Playcode offers your app through Playcode AI Gateway, with one API for all of them. generate() answers once; stream() gives the same answer as it is written. Each call is paid from your workspace's credits, at the model's price, and its result says what it cost.

On this page

Choosing a model

#

Your app names a model by its id. Read the ids, and what each model can do, from Playcode at run time:

const readers = await playcode.ai.models({
  accepts: 'pdf',
  recommendedFor: 'documents',
})

Each model card holds:

Field What it says
id What you pass as model
vendor Who makes the model: its maker
api The interface the model is called on: anthropic, openai-chat, openai-responses or google. playcode.ai calls each the same way
accepts What it reads: text, image, pdf, with the limits of each. A missing kind is not supported
context The most tokens one call can send (inputTokens) and answer (outputTokens)
thinking The thinking levels it supports, and its default
price Credits per token, one meter per kind of token
region Where its maker processes the call: USA, EU, global (anywhere the maker runs, with no promise of a region) or unknown
recommendedFor What Playcode's measured runs found it best at, such as documents
lifecycle stage: preview, ga or deprecated; replacedBy, its successor; retiresAt, the date it retires

The filters are accepts, recommendedFor, vendor and api, and together they narrow the list. To compare prices, read the meters in price.meters: token_in, token_out, token_cache_read, token_cache_write and token_cache_write_1h, each in credits per token:

import type { ModelCard } from '@playcode/sdk'

const rate = (card: ModelCard, unit: string) => {
  const meter = card.price.meters.find((candidate) => candidate.unit === unit)
  return meter ? Number(meter.creditsPerUnit) : Infinity
}

const cheapest = readers
  .filter((card) => card.lifecycle.stage !== 'deprecated')
  .sort((a, b) => rate(a, 'token_in') - rate(b, 'token_in'))[0]

Once a feature works on a model, keep that model's id in one constant. When its lifecycle.stage is deprecated, move to lifecycle.replacedBy before lifecycle.retiresAt.

In the samples below, model is the id you chose, and reader is one whose card reads PDFs.

Text

#
const { text } = await playcode.ai.generate({
  model,
  system: 'You write product descriptions. Two sentences, plain text.',
  prompt: 'A red ceramic mug, hand-glazed, 350 ml.',
})

For a conversation, send messages instead of prompt. A result's messages are what the call added, so the next call sends the history with them:

import type { AiMessage } from '@playcode/sdk'

const history: AiMessage[] = [
  { role: 'user', content: 'Gift ideas for my sister?' },
]
const first = await playcode.ai.generate({ model, messages: history })
history.push(...first.messages, { role: 'user', content: 'Under 30 euros.' })
const second = await playcode.ai.generate({ model, messages: history })

A history is portable: one written on one model replays on another.

Typed data

#

Give a schema, and the answer comes back as a checked, typed object:

import { z } from 'zod'

const Lead = z.object({
  name: z.string(),
  company: z.string().optional(),
  budget: z.number().describe('In euros'),
  wantsCall: z.boolean(),
})

const { object } = await playcode.ai.generate({
  model,
  prompt: contactFormText,
  schema: Lead,
})
console.log(object.budget) // a number

A schema is a zod schema (v3 or v4) or a JSON Schema object. An answer that does not match throws PlaycodeApiError with the code invalid_output, and that call is still paid: the model was called. Say what the model cannot know in system, such as "dates on these documents are day-month-year".

Images and PDFs

#
const { object } = await playcode.ai.generate({
  model: reader,
  system: 'You read invoices. Dates on these invoices are day-month-year.',
  prompt: 'Read this invoice.',
  files: [{ data: pdfBytes, mediaType: 'application/pdf' }],
  schema: z.object({ number: z.string(), total: z.number() }),
})
  • data is bytes, base64 text, a data: URL, or an https: URL that the model's maker fetches itself.
  • mediaType is image/jpeg, image/png, image/webp, image/gif or application/pdf. filename is the name the model sees, where its wire has a place for one: PDFs.
  • The SDK checks each file against the model's card before it sends anything, and names a file the model cannot read.
  • One request carries at most 32 MiB. Files travel as base64, so that is about 24 MiB of file bytes.
  • In the browser, prepareImage(file) from @playcode/sdk/client scales a photo down to at most 2,000 px on its long edge, as JPEG, before the upload.

The whole path, from the file picker to checked fields: Read images and PDFs in your app.

Streaming

#
const prompt = 'Write a short welcome for a bakery website.'

let credits = 0
for await (const event of playcode.ai.stream({ model, prompt })) {
  if (event.type === 'text') process.stdout.write(event.text)
  if (event.type === 'done') credits = event.result.usage.credits
}
console.log(`\n${credits} credits`)

The events are text, toolCall, toolResult, usage, step, compaction, fallback, subagent and, last, done with the result. Leaving the loop early stops the call. A stream can be read once.

Your functions as tools

#

With tools, the model can call your code to get what it needs, then answer:

import { tool } from '@playcode/sdk'

const { text } = await playcode.ai.generate({
  model,
  prompt: 'Is order A-1042 shipped yet?',
  tools: {
    findOrder: tool({
      description: 'An order by its number: its status and its items.',
      input: z.object({ number: z.string() }),
      execute: ({ number }) => db.orders.find(number),
    }),
  },
})

generate() then makes up to 10 model calls by default (maxSteps), each one paid. Its result's finish says why it stopped: stop, length, content-filter, max-steps, budget or stuck, each explained in How a run ends. For an assistant that holds a conversation, or a tool a person approves first, use an agent: Build an agent.

Thinking

#

thinking is off, low, medium, high or max. The SDK moves it to the nearest level the model's card lists. Leave it out to use the model's default, which measured best on Playcode's own runs. A higher level writes more tokens, so it costs more.

What a call costs

#
Field What it is
usage.credits What the call cost, at the model's price
usage.inputTokens, usage.outputTokens Every token read and written, thinking and cache included
usage.cacheReadTokens, usage.cacheWriteTokens The part of the input read from, or written to, the prompt cache
usageIds One id per model call: quote one when you ask about a charge

To cap a cost, set maxOutputTokens, the longest answer one model call may write, and budget: { credits }. Before each model call, the SDK compares what the calls so far cost with the budget, and stops once it is spent. So a call can end above its budget, by its last step: the step's model call, and a brief or a sub-agent's calls made in that step.

To see what each of your end users costs, pass user, your own id for the person a call serves: What each user costs.

On Claude models, cache marks the repeated part of the prompt, so the next call reads it at the cache price. A call without tools marks nothing by default: a cache write can cost more than plain input, and nothing may read it back. Pass cache: true when many calls repeat one long system prompt. A call with tools marks by default, because each step reads what the step before wrote. GPT and Gemini models cache a long repeated prefix on their own, marked or not.

Errors, retries and stops

#

A retry is a new paid call, so the SDK retries only what Playcode shows was not charged: Playcode's own rate limit, when it asks to wait 30 seconds or less; a model maker's rate limit or server error, which Playcode voided; or a maker Playcode could not reach. It retries at most 2 times, and never a stream that broke after it started. Do not wrap AI calls in a retry loop of your own.

signal stops a call at once, and generate() then throws the signal's reason, or an AbortError. A call the model had started is still charged for what it read.

Error When
PlaycodeApiError Playcode or the model's maker said no. status, code, message in Playcode's own words, source (playcode, or provider for the model's maker), retryAfterMs, usageId
PlaycodeConnectionError No answer came (the network, TLS or a timeout), or the stream broke off mid-answer, which may be charged
PlaycodeConfigError The call could not be built: no key, or a file the model cannot read

Every code and what to do about it: SDK reference.

A model maker's own SDK

#

For a feature playcode.ai does not cover, point the maker's official SDK at Playcode. The key and the credits stay the same:

import Anthropic from '@anthropic-ai/sdk'

const anthropic = new Anthropic(playcode.ai.endpoint('anthropic'))

The other values are openai-chat and openai-responses for the openai package, and google for @google/genai, which takes the address as httpOptions.baseUrl. The card's api says which one a model uses. Tools that a model's maker runs and bills itself, such as its web search, are refused with server_tool_not_supported: write the tool as one of your functions instead.