---
title: puter.ai.chat()
description: Chat with AI models, analyze images and videos, and perform function calls using 500+ models from OpenAI, Anthropic, Google, and more.
platforms: [websites, apps, nodejs, workers]
---

Given a prompt returns the completion that best matches the prompt.

## Syntax

```js
puter.ai.chat(prompt)
puter.ai.chat(prompt, options = {})
puter.ai.chat(prompt, testMode = false, options = {})
puter.ai.chat(prompt, media, testMode = false, options = {})
puter.ai.chat(prompt, [mediaURLArray], testMode = false, options = {})
puter.ai.chat([messages], testMode = false, options = {})
```

## Parameters

#### `prompt` (String)

A string containing the prompt you want to complete.

#### `options` (Object) (Optional)

An object containing the following properties:

- `model` (String) - The model you want to use for the completion. If not specified, defaults to `gpt-5-nano`. More than 500 models are available from vendors including OpenAI, Anthropic, Google, Alibaba Cloud, xAI, Mistral, OpenRouter, Infron, and others. For a full list, see the [AI models list](https://developer.puter.com/ai/models/) page.
- `provider` (String) (Optional) - Pin the request to a specific vendor, for example `openrouter` or `infron`. Without it, Puter selects a vendor for the requested model. Call [`puter.ai.listModelProviders()`](/AI/listModelProviders) for the available values, and [`puter.ai.listModels(provider)`](/AI/listModels) for the models a given vendor serves.
- `stream` (Boolean) - A boolean indicating whether you want to stream the completion. Defaults to `false`.
- `max_tokens` (Number) - The maximum number of tokens to generate in the completion. By default, the specific model's maximum is used.
- `temperature` (Number) - A number between 0 and 2 indicating the randomness of the completion. Lower values make the output more focused and deterministic, while higher values make it more random. By default, the specific model's temperature is used.
- `tools` (Array) (Optional) - Function definitions the AI can call. See [Function Calling](#function-calling) for details.
- `reasoning_effort` / `reasoning.effort` (String) (Optional) - Controls how much effort reasoning models spend thinking. Supported values: `none`, `minimal`, `low`, `medium`, `high`, and `xhigh`. Lower values give faster responses with less reasoning. OpenAI models only.
- `verbosity` / `text.verbosity` (String) (Optional) - Controls how long or short responses are. Supported values: `low`, `medium`, and `high`. Lower values give shorter responses. OpenAI models only.
- `compaction` (Boolean | Object) (Optional) - Opt into inline context compaction for long conversations. Pass `true` to enable it with provider defaults, or `{ trigger_tokens: number }` to set the token threshold at which earlier context is summarized. When the model compacts, you receive a `compaction` chunk while streaming (or a `compaction` field on the result when not streaming) containing an opaque `encrypted_content` summary. Resend that item in `messages` on the next turn in place of the summarized history. The compaction chunk shape is identical across providers, so the same code works whether `model` is an OpenAI or Anthropic model. See [Compaction](#compaction).

#### `testMode` (Boolean) (Optional)

A boolean indicating whether you want to use the test API. Defaults to `false`. This is useful for testing your code without using up API credits.

#### `media` (String | File)

A string containing the URL or Puter path of an image or video, or a `File` object containing the media you want to provide as context for the completion.

#### `mediaURLArray` (Array)

An array of strings containing the URLs of images or videos you want to provide as context for the completion.

#### `messages` (Array)

An array of objects containing the messages you want to complete. Each object must have a `role` and a `content` property. The `role` property must be one of `system`, `assistant`, `user`, or `tool`. The `content` property can be:

1. A string containing the message text
2. An array of content objects for multimodal messages

When using an array of content objects, each object can have:

- `type` (String) - The type of content:
  - `"text"` - Text content
  - `"file"` - File content
- `text` (String) - The text content (required when type is "text")
- `puter_path` (String) - The path to the file in Puter's file system (required when type is "file")

An example of a valid `messages` parameter with text only:

```js
[
  {
    role: "system",
    content: "Hello, how are you?",
  },
  {
    role: "user",
    content: "I am doing well, how are you?",
  },
];
```

An example with mixed content including files:

```js
[
  {
    role: "user",
    content: [
      {
        type: "file",
        puter_path: "~/Desktop/document.pdf",
      },
      {
        type: "text",
        text: "Please summarize this document",
      },
    ],
  },
];
```

Providing a messages array is especially useful for building chatbots where you want to provide context to the completion.

## Return value

Returns a `Promise` that resolves to either:

- A [`ChatResponse`](/Objects/chatresponse) object containing the chat response data, or
- An async iterable object of [`ChatResponseChunk`](/Objects/chatresponsechunk) (when `stream` is set to `true`) that you can use with a `for await...of` loop to receive the response in parts as they become available.

In case of an error, the `Promise` will reject with an error message.

## Vendors

We use different vendors for different models and try to use the best vendor available at the time of the request. Vendors currently include Alibaba Cloud, Anthropic, Azure OpenAI, DeepSeek, Google, Infron, MiniMax, Mistral, Moonshot AI, OpenAI, OpenRouter, Together AI, xAI, and Z.AI. Call [`puter.ai.listModelProviders()`](/AI/listModelProviders) for the current list, or pass `provider` in the options object to pin a request to one of them.

## Function Calling

Function calling (also known as tool calling) allows AI models to request data or perform actions by calling functions you define. This enables the AI to access real-time information, interact with external systems, and perform tasks beyond its training data.

1. **Define tools** - Create function specifications in the `tools` array passed to `puter.ai.chat()`
2. **AI requests a tool call** - If the AI determines it needs to call a function, it responds with a `tool_calls` array instead of a text message
3. **Execute the function** - Your code matches the requested function and runs it with the provided arguments
4. **Send the result back** - Pass the function result back to the AI with `role: "tool"`
5. **AI responds** - The AI uses the tool result to generate its final response

Tools are defined in the `tools` parameter as an array of function specifications:

- `type` (String) - Must be `"function"`
- `function.name` (String) - The function name (e.g., `"get_weather"`)
- `function.description` (String) - Description of what the function does and when to use it
- `function.parameters` (Object) - [JSON Schema](https://json-schema.org/) object defining the function's input arguments
- `function.strict` (Boolean) (Optional) - Whether to enforce strict parameter validation

When the AI wants to call a function, the response includes `message.tool_calls`. Each tool call contains:

- `id` (String) - Unique identifier for this tool call (used when sending results back)
- `function.name` (String) - The name of the function to call
- `function.arguments` (String) - JSON string containing the function arguments

After executing the function, send the result back by including a message with:

- `role` (String) - Must be `"tool"`
- `tool_call_id` (String) - The `id` from the tool call
- `content` (String) - The function result as a string

See the [Function Calling example](/playground/ai-function-calling/) for a complete working implementation.

### Web Search

Specific to OpenAI models, you can use the built-in web search tool, allowing the AI to access up-to-date information from the internet.

Pass in the `tools` parameter with the type of `web_search`.

```js
{
  model: 'openai/gpt-5.3-chat',
  tools: [{type: "web_search"}]
}
```

The code implementation is available in our [web search example](/playground/ai-web-search/).

List of OpenAI models that support the web search can be found in their [API compatibility documentation](https://platform.openai.com/docs/guides/tools-web-search#api-compatibility).

## Prompt Caching

Specific to Anthropic models, you can use the cache control feature, allowing you to optimize costs for repeated prompts.

Pass in the `cache_control` parameter inside the object in the `messages` array.

```js
[
    {
        role: 'system',
        content: 'a really long system prompt',
        cache_control: { type: "ephemeral" }
    },
    {
        role: 'user',
        content: '<your message>'
    },
]
```

You can find the implementation in our [prompt caching example](/playground/ai-claude-cache-control/). Find more details about cache control in [Anthropic documentation](https://platform.claude.com/docs/en/build-with-claude/prompt-caching).

## Compaction

For long, multi-turn conversations that you keep on the client, enable
`compaction` so the model can summarize earlier context before it overflows the
context window. When it fires, the model summarizes the older turns into a single
**compaction artifact** and answers from that summary instead of the full
history — so the request stays small.

You get the artifact back as a `compaction` item (a stream chunk when streaming,
or `result.compaction` when not). On the next turn you carry it forward **as the
first block of the assistant turn that produced it**, together with that turn's
reply text — the artifact stands in for the older turns it summarized, and the
recent exchange is preserved. The item shape is the same across providers, so the
same code works for OpenAI and Anthropic models.

#### Enabling it

Pass `compaction` in the options object:

- `compaction: true` — enable with provider defaults.
- `compaction: { trigger_tokens: 60000 }` — set the token threshold at which the
  model compacts.

The artifact is `{ type: 'compaction', id, encrypted_content }`. `encrypted_content`
is an opaque payload; treat it as a black box and just carry it forward.

#### Streaming

```js
const resp = await puter.ai.chat(messages, {
    model: 'gpt-5.5',                       // or 'claude-opus-4-8' — same code
    stream: true,
    compaction: { trigger_tokens: 60000 },
});

let text = '';
let compaction = null;
for await ( const part of resp ) {
    if ( part.type === 'text' )             text += part.text;
    else if ( part.type === 'compaction' )  compaction = part;                 // { type, id, encrypted_content }
    else if ( part.type === 'error' )       console.error('stream error:', part.message);
}

// Next turn: rebuild the assistant turn from the compaction artifact + the reply
// text it came with (artifact first), then add the new user message. The artifact
// replaces the older compacted turns; the recent exchange is kept. Keep
// compaction enabled so it can compact again later.
if ( compaction ) {
    const next = await puter.ai.chat(
        [
            { role: 'system', content: 'You are a helpful assistant.' },
            {
                role: 'assistant',
                content: [ compaction, { type: 'text', text } ],
            },
            { role: 'user', content: 'now compare the two approaches' },
        ],
        { model: 'gpt-5.5', stream: true, compaction: true }
    );
    for await ( const part of next ) {
        if ( part.type === 'text' ) document.write(part.text);
    }
}
```

#### Non-streaming

```js
const result = await puter.ai.chat(messages, {
    model: 'gpt-5.5',
    compaction: { trigger_tokens: 60000 },
});
console.log(result.message.content);

if ( result.compaction ) {
    // result.compaction is { type: 'compaction', id, encrypted_content } — the
    // same item you get from the stream. Rebuild the assistant turn from it plus
    // the reply text (artifact first), then add the new user message:
    const next = await puter.ai.chat(
        [
            { role: 'system', content: 'You are a helpful assistant.' },
            {
                role: 'assistant',
                content: [
                    result.compaction,
                    { type: 'text', text: result.message.content },
                ],
            },
            { role: 'user', content: 'now compare the two approaches' },
        ],
        { model: 'gpt-5.5', compaction: true }
    );
    console.log(next.message.content);
}
```

#### Notes

- **Keep `compaction` enabled on every turn** of the conversation so it can
  compact again as the conversation keeps growing.
- **Carry the artifact as the first block of its assistant turn**, alongside that
  turn's reply text, then continue with new turns. The artifact replaces the older
  compacted turns; don't drop the recent exchange.
- **It only fires once the context is large enough.** Anthropic models require a
  minimum threshold of **50,000 tokens**, and the conversation must actually
  exceed your `trigger_tokens`. OpenAI models don't enforce that floor, so they
  can compact smaller conversations. If nothing compacts, your input was below
  the threshold.
- **Handle the `error` chunk** when streaming — provider errors (e.g. a
  `trigger_tokens` below a provider's minimum) arrive as an `error` chunk, not a
  thrown exception.

## Image Generation (Gemini Image Models)

Certain Gemini models can generate and edit images as part of a chat conversation. These models accept text and image inputs, and return text and images in the response.

#### Supported Models

| Model | Quality Levels |
|-------|---------------|
| `gemini-2.5-flash-image` | — |
| `gemini-3-pro-image-preview` | `1K`, `2K`, `4K` |
| `gemini-3.1-flash-image-preview` | `512`, `1K`, `2K`, `4K` |

#### Options

Pass `image_config` in the options object to control image output:

| Option | Type | Description |
|--------|------|-------------|
| `image_config.aspect_ratio` | `String` | Aspect ratio (e.g. `"16:9"`, `"1:1"`, `"9:16"`) |
| `image_config.image_size` | `String` | Output quality/resolution. Must be one of the model's supported quality levels |

For available aspect ratios and image sizes per model, see the [Gemini Image Generation documentation](https://ai.google.dev/gemini-api/docs/image-generation#aspect_ratios_and_image_size).

#### Response Format

The response includes an `images` array on the message when the model generates images:

```js
{
    message: {
        role: "assistant",
        content: "Here is your image.",
        images: [
            {
                type: "image_url",
                image_url: { url: "data:image/png;base64,..." }
            }
        ]
    }
}
```

#### Multi-Turn Image Editing

You can send generated images back in the conversation to iteratively edit them. Include the image in an `assistant` message using the `image_url` content type, and pass the `thoughtSignature` from the previous response to maintain editing context:

```js
const previousImage = result.message.images[0].image_url.url;
const thoughtSignature = result.message.images[0].thoughtSignature;

const result2 = await puter.ai.chat([
    { role: "user", content: "Create an infographic about photosynthesis" },
    { role: "assistant", content: [
        { type: "text", text: "Here is the infographic." },
        { type: "image_url", image_url: { url: previousImage }, thoughtSignature },
    ]},
    { role: "user", content: "Translate all text to Spanish" },
], {
    model: "gemini-3.1-flash-image-preview",
    image_config: { aspect_ratio: "16:9", image_size: "2K" },
});

const editedImage = result2.message.images[0].image_url.url;
```

The code implementation is available in our [image generation example](/playground/ai-image-chat/) and [multi-turn image editing example](/playground/ai-image-edit/).

#### Streaming

Image generation works with `stream: true`. Image chunks arrive as `image` events:

```js
const resp = await puter.ai.chat("Draw a cat", {
    model: "gemini-3.1-flash-image-preview",
    stream: true,
});

for await (const part of resp) {
    if (part.text) console.log(part.text);
    if (part.image) {
        // part.image is { type: "image_url", image_url: { url: "data:..." } }
        const img = document.createElement("img");
        img.src = part.image.image_url.url;
        document.body.appendChild(img);
    }
}
```

## Examples

<strong class="example-title">Ask GPT-5.4 nano a question</strong>

```html;ai-chatgpt
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        puter.ai.chat(`What is life?`, { model: "gpt-5.4-nano" }).then(puter.print);
    </script>
</body>
</html>
```

<strong class="example-title">Image Analysis</strong>

```html;ai-gpt-vision
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <img src="https://assets.puter.site/doge.jpeg" style="display:block;">
    <script>
        puter.ai
            .chat(`What do you see?`, `https://assets.puter.site/doge.jpeg`, {
                model: "gpt-5.4-nano",
            })
            .then(puter.print);
    </script>
</body>
</html>
```

<strong class="example-title">Video Analysis</strong>

```html;ai-video-analysis
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        puter.ai
            .chat(`What do you see?`, `https://assets.puter.site/puppy.mp4`, {
                model: "reka/reka-edge",
            })
            .then(puter.print);
    </script>
</body>
</html>
```

<strong class="example-title">Stream the response</strong>

```html;ai-chat-stream
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
    (async () => {
        const resp = await puter.ai.chat('Tell me in detail what Rick and Morty is all about.', {model: 'gemini-3.1-flash-lite', stream: true });
        for await ( const part of resp ) document.write(part?.text.replaceAll('\n', '<br>'));
    })();
    </script>
</body>
</html>
```

<strong class="example-title">Function Calling</strong>

```html;ai-function-calling
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        // Mock weather function
        function getWeather(location) {
            return location + ': 22°C, Sunny';
        }

        // Define the tool
        const tools = [{
            type: "function",
            function: {
                name: "get_weather",
                description: "Get current weather for a location",
                parameters: {
                    type: "object",
                    properties: {
                        location: { type: "string", description: "City name" }
                    },
                    required: ["location"]
                }
            }
        }];

        (async () => {
            const question = "What's the weather in Paris?";
            puter.print("Question: " + question + "<br/>");
            puter.print("(Loading...)<br/>");

            // Call AI with tools
            const response = await puter.ai.chat(question, { tools });

            // Check if AI wants to call a function
            if (response.message.tool_calls?.length > 0) {
                const toolCall = response.message.tool_calls[0];
                const args = JSON.parse(toolCall.function.arguments);
                const weatherData = getWeather(args.location);

                // Send result back to AI
                const finalResponse = await puter.ai.chat([
                    { role: "user", content: question },
                    response.message,
                    { role: "tool", tool_call_id: toolCall.id, content: weatherData }
                ]);

                puter.print("Answer: " + finalResponse);
            } else {
                // If the AI responds directly without calling a tool, print its message
                puter.print("Answer: " + response);
            }
        })();
    </script>
</body>
</html>
```

<strong class="example-title">Streaming Function Calling</strong>

```html;ai-streaming-function-calling
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        // Define the tool
        const tools = [{
            type: "function",
            function: {
                name: "get_weather",
                description: "Get current weather for a location",
                parameters: {
                    type: "object",
                    properties: {
                        location: { type: "string", description: "City name" }
                    },
                    required: ["location"]
                }
            }
        }];

        // Mock weather function
        function getWeather(location) {
            return `The weather in ${location} is 22°C and Sunny.`;
        }

        (async () => {
            const question = "What's the weather in Paris?";
            puter.print(`Question: ${question}<br/>`);

            // 1. Call AI with stream: true AND tools
            const response = await puter.ai.chat(question, { 
                tools,
                stream: true 
            });

            // 2. Iterate through the stream
            for await (const part of response) {
                
                // Standard Text Stream
                if (part.type === 'text') {
                    puter.print(part.text);
                }
                
                // Tool Call Detected
                else if (part.type === 'tool_use') {
                    const toolCall = part;
                    const funcName = toolCall.name;
                    const args = toolCall.input; // Already parsed: { location: "Paris" }

                    puter.print(`<br/>[System] Calling tool: ${funcName} with args: ${JSON.stringify(args)}<br/>`);

                    // Execute the local function
                    let result;
                    if (funcName === 'get_weather') {
                        result = getWeather(args.location);
                    }

                    // Send the tool result back to the AI to get the final answer
                    const finalResponse = await puter.ai.chat([
                        { role: "user", content: question },
                        { role: "assistant", tool_calls: [{
                            id: toolCall.id,
                            type: "function",
                            function: { name: funcName, arguments: JSON.stringify(args) }
                        }]},
                        { role: "tool", tool_call_id: toolCall.id, content: result }
                    ], { stream: true });

                    for await (const finalPart of finalResponse) {
                        if (finalPart.text) puter.print(finalPart.text);
                    }
                }
            }
        })();
    </script>
</body>
</html>
```

<strong class="example-title">Web Search</strong>

```html;ai-web-search
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        puter.print(`Loading...`);
        puter.ai
            .chat("Summarize what the User-Pays Model is: https://docs.puter.com/user-pays-model/", {
                model: "openai/gpt-5.3-chat",
                tools: [{ type: "web_search" }],
            })
            .then(puter.print);
    </script>
</body>
</html>
```

<strong class="example-title">Prompt caching with Claude</strong>

```html;ai-claude-cache-control
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        const systemPrompt = `You are an expert customer support agent for Acme Corporation, a global technology company specializing in cloud computing, AI solutions, and enterprise software.
COMPANY OVERVIEW:
Acme Corporation was founded in 2010 and is headquartered in San Francisco, California. We serve over 50,000 enterprise customers across 120 countries. Our products include AcmeCloud (infrastructure-as-a-service), AcmeBrain (AI/ML platform), AcmeFlow (workflow automation), and AcmeShield (cybersecurity suite). Our mission is to empower businesses of all sizes with reliable, scalable, and secure technology solutions.

PRODUCT POLICIES:
Policy 1 - Subscription Tiers: We offer four subscription tiers: Starter ($29/month per user), Professional ($79/month per user), Enterprise ($149/month per user), and Ultimate ($299/month per user). Each tier includes different levels of API access, storage, and support. Starter includes 10GB storage and email support. Professional includes 100GB storage, priority email, and chat support. Enterprise includes 1TB storage, 24/7 phone support, and a dedicated account manager. Ultimate includes unlimited storage, 24/7 priority support, a dedicated account manager, and custom SLA agreements.
Policy 2 - Refund Policy: All subscriptions come with a 30-day money-back guarantee. After 30 days, refunds are prorated based on remaining time. Annual subscriptions receive a 20% discount but refunds after 30 days are calculated at the monthly rate. Enterprise and Ultimate customers may negotiate custom refund terms with their account manager. Refund requests must be submitted through the billing portal or by contacting support.
Policy 3 - Data Retention: Customer data is retained for the duration of the subscription plus 90 days after cancellation. After 90 days, all data is permanently deleted unless the customer requests an extension. Backups are maintained for 30 days on Starter and Professional tiers, 90 days on Enterprise, and 365 days on Ultimate. Customers can export their data at any time through the dashboard or API.
Policy 4 - Service Level Agreements: Starter tier: 99.5% uptime guarantee. Professional tier: 99.9% uptime guarantee. Enterprise tier: 99.95% uptime guarantee. Ultimate tier: 99.99% uptime guarantee with custom SLA options. SLA credits are calculated as 10x the downtime duration applied to the next billing cycle. Scheduled maintenance windows are excluded from SLA calculations and are announced 72 hours in advance.
Policy 5 - Security and Compliance: All tiers include SOC 2 Type II compliance, GDPR compliance, and TLS 1.3 encryption. Enterprise and Ultimate tiers additionally include HIPAA compliance, FedRAMP authorization (in progress), and custom data residency options. Two-factor authentication is available on all tiers and mandatory on Enterprise and Ultimate. SSO integration via SAML 2.0 and OIDC is available on Professional tier and above.
Policy 6 - API Rate Limits: Starter: 100 requests per minute. Professional: 1,000 requests per minute. Enterprise: 10,000 requests per minute. Ultimate: 100,000 requests per minute with burst capacity up to 500,000. Rate limit increases can be requested by Enterprise and Ultimate customers through their account manager. API usage is monitored and customers approaching their limits are notified automatically.
Policy 7 - Support Escalation: Level 1 (General Support): Available to all tiers, response within 24 hours for Starter, 4 hours for Professional, 1 hour for Enterprise, and 15 minutes for Ultimate. Level 2 (Technical Specialist): Available to Professional and above, response within 8 hours for Professional, 2 hours for Enterprise, and 30 minutes for Ultimate. Level 3 (Engineering Team): Available to Enterprise and above, response within 4 hours for Enterprise and 1 hour for Ultimate.
Policy 8 - Account Management: Each Enterprise and Ultimate customer is assigned a dedicated account manager who conducts quarterly business reviews, provides usage analytics and optimization recommendations, assists with onboarding and training for new team members, and coordinates with product teams on feature requests. Account managers are available during business hours (9 AM - 6 PM in the customer's local timezone) and can be reached via email, phone, or the customer portal. For urgent issues outside business hours, Enterprise and Ultimate customers can use the 24/7 emergency hotline.
`;

        async function askQuestion(question) {
            const response = await puter.ai.chat(
                [
                    {
                        role: "system",
                        content: systemPrompt,
                        cache_control: { type: "ephemeral" },
                    },
                    { role: "user", content: question },
                ],
                { model: "claude-sonnet-4-6" }
            );
            return response.message.content[0].text;
        }

        (async () => {
            puter.print("<b>Call 1 (cache write: first time processing system prompt)</b><br>");
            const r1 = await askQuestion("How do I get a refund?");
            puter.print(r1 + "<br><br>");

            puter.print("<b>Call 2 (cache hit: system prompt reused from cache)</b><br>");
            const r2 = await askQuestion("What are your API rate limits?");
            puter.print(r2 + "<br><br>");

            puter.print("<b>Call 3 (cache hit)</b><br>");
            const r3 = await askQuestion("What is your data retention policy?");
            puter.print(r3 + "<br><br>");
        })();
    </script>
</body>
</html>
```

<strong class="example-title">Image Generation</strong>

```html;ai-image-chat
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
    (async () => {
        puter.print("Generating image...<br>");
        const result = await puter.ai.chat("Draw a cute cat wearing a top hat", {
            model: "gemini-3.1-flash-image-preview",
        });
        puter.print(result.message.content + "<br>");
        if (result.message.images?.length > 0) {
            const img = document.createElement("img");
            img.src = result.message.images[0].image_url.url;
            img.style.maxWidth = "512px";
            document.body.appendChild(img);
        }
    })();
    </script>
</body>
</html>
```

<strong class="example-title">Multi-Turn Image Editing</strong>

```html;ai-image-edit
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
    (async () => {
        const model = "gemini-3.1-flash-image-preview";

        // Step 1: Generate initial image
        puter.print("Step 1: Generating infographic...<br>");
        const r1 = await puter.ai.chat("Create a simple infographic about photosynthesis", {
            model,
            image_config: { image_size: "1K" },
        });
        const img1 = r1.message.images?.[0]?.image_url?.url;
        const thoughtSignature1 = r1.message.images?.[0]?.thoughtSignature;
        if (img1) {
            const el = document.createElement("img");
            el.src = img1;
            el.style.maxWidth = "512px";
            document.body.appendChild(el);
        }

        // Step 2: Edit the image in a follow-up turn
        puter.print("<br>Step 2: Translating to Spanish...<br>");
        const r2 = await puter.ai.chat([
            { role: "user", content: "Create a simple infographic about photosynthesis" },
            { role: "assistant", content: [
                { type: "text", text: r1.message.content },
                { type: "image_url", image_url: { url: img1 }, thoughtSignature: thoughtSignature1 },
            ]},
            { role: "user", content: "Translate all text to Spanish. Keep everything else the same." },
        ], {
            model,
            image_config: { aspect_ratio: "16:9", image_size: "2K" },
        });
        const img2 = r2.message.images?.[0]?.image_url?.url;
        if (img2) {
            const el = document.createElement("img");
            el.src = img2;
            el.style.maxWidth = "512px";
            document.body.appendChild(el);
        }
        puter.print("<br>Done!");
    })();
    </script>
</body>
</html>
```

<strong class="example-title">Working with Files</strong>

```html;ai-resume-analyzer
<!DOCTYPE html>
<html>
<head>
    <title>Resume Analyzer</title>
    <script src="https://js.puter.com/v2/"></script>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 20px auto; padding: 20px;}
        .container { border: 1px solid #ccc; padding: 20px; border-radius: 5px;}
        .upload-area {border: 2px dashed #ccc; padding: 40px; text-align: center; margin: 20px 0; border-radius: 5px; cursor: pointer;  transition: border-color 0.3s;}
        .upload-area:hover {border-color: #007bff;}
        .upload-area.dragover { border-color: #007bff; background-color: #f8f9fa;}
        input[type="file"] { display: none;}
        button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px;}
        button:disabled { background: #ccc; }
        #response { margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 5px; display: none; }
        .file-name { margin-top: 10px; font-style: italic; color: #666; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Resume Analyzer</h1>
        <p>Upload your resume (PDF, DOC, or TXT) and get a quick analysis of your key strengths in two sentences.</p>

        <div class="upload-area" onclick="document.getElementById('fileInput').click()">
            <p>Click here to upload your resume or drag and drop</p>
            <input type="file" id="fileInput" accept=".pdf,.doc,.docx,.txt" />
        </div>

        <div class="file-name" id="fileName" style="display: none;"></div>

        <button id="analyzeBtn" disabled>Analyze My Resume</button>

        <div id="response"></div>
    </div>

    <script>
        let uploadedFile = null;

        // File upload handling
        const fileInput = document.getElementById('fileInput');
        const uploadArea = document.querySelector('.upload-area');
        const fileName = document.getElementById('fileName');
        const analyzeBtn = document.getElementById('analyzeBtn');
        const response = document.getElementById('response');

        fileInput.addEventListener('change', handleFileSelect);
        uploadArea.addEventListener('dragover', handleDragOver);
        uploadArea.addEventListener('drop', handleDrop);

        function handleFileSelect(e) {
            const file = e.target.files[0];
            if (file) {
                uploadedFile = file;
                fileName.textContent = `Selected: ${file.name}`;
                fileName.style.display = 'block';
                analyzeBtn.disabled = false;
            }
        }

        function handleDragOver(e) {
            e.preventDefault();
            uploadArea.classList.add('dragover');
        }

        function handleDrop(e) {
            e.preventDefault();
            uploadArea.classList.remove('dragover');

            const file = e.dataTransfer.files[0];
            if (file) {
                uploadedFile = file;
                fileName.textContent = `Selected: ${file.name}`;
                fileName.style.display = 'block';
                analyzeBtn.disabled = false;
            }
        }

        // Remove dragover class when drag leaves
        uploadArea.addEventListener('dragleave', () => {
            uploadArea.classList.remove('dragover');
        });

        // Analyze resume
        analyzeBtn.addEventListener('click', async () => {
            if (!uploadedFile) return;

            analyzeBtn.disabled = true;
            analyzeBtn.textContent = 'Analyzing...';
            response.style.display = 'none';

            try {
                // First, upload the file to Puter
                const puterFile = await puter.fs.write(`temp_resume_${Date.now()}.${uploadedFile.name.split('.').pop()}`,
                    uploadedFile
                );

                const uploadedPath = puterFile.path;

                // Analyze the resume with AI
                const completion = await puter.ai.chat([
                    {
                        role: 'user',
                        content: [
                            {
                                type: 'file',
                                puter_path: uploadedPath
                            },
                            {
                                type: 'text',
                                text: 'Please analyze this resume and suggest how to improve it. Only a few sentences are needed.'
                            }
                        ]
                    }
                ], { model: 'claude-sonnet-4-6', stream: true });

                let text = '';

                // Display the response
                for await ( const part of completion ) {
                    text += part?.text;
                    response.innerHTML = text;
                }

                response.style.display = 'block';

                // Clean up the temporary file
                await puter.fs.delete(uploadedPath);

            } catch (error) {
                response.innerHTML = `<strong>Error:</strong><br>${error.message}`;
                response.style.display = 'block';
            }

            analyzeBtn.disabled = false;
            analyzeBtn.textContent = 'Analyze My Resume';
        });
    </script>
</body>
</html>
```
