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() }),
})
datais bytes, base64 text, adata:URL, or anhttps:URL that the model's maker fetches itself.mediaTypeisimage/jpeg,image/png,image/webp,image/giforapplication/pdf.filenameis 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/clientscales 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.