---
title: "Stream generate content"
description: "Generate content (Gemini protocol, streamed)."
canonical: "https://pirouter.ai/docs/api/stream-generate-content"
last-updated: "2026-10-13"
lang: "en"
---

# Stream generate content

Generate content (Gemini protocol, streamed).

`POST https://api.pirouter.ai/v1beta/models/{model}:streamGenerateContent`

Gemini-compatible streamGenerateContent. The model and the streaming choice both come from the path; the body carries neither. A model is served only on the protocols it is registered for; calling one that does not accept this protocol is `validation_failed` (422). This path always streams; a `stream` key in the body is not read. Only the fields listed in the request schema are read by the platform; every other field (`generationConfig`, `tools`, ...) is forwarded to the upstream provider as-is, and so are query parameters such as `alt=sse`.

The key goes in `Authorization` or `X-Api-Key`. The `x-goog-api-key` header and the `key` query parameter are not read.

## Authorization

Send your API key in one of these headers.

| Header | Value | Description |
| --- | --- | --- |
| `Authorization` | `Bearer sk-pi-…` | Platform-issued API key with the `sk-pi-` prefix, sent as a Bearer token. |
| `X-Api-Key` | `sk-pi-…` | The same platform-issued `sk-pi-` key, sent in `X-Api-Key`. Read only when `Authorization` is absent or empty; when both are sent, `Authorization` wins. A bare key without the `Bearer ` prefix is accepted in either header. |

## Path parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | The platform model slug in `vendor/model` form. The slash is part of the value and is sent unescaped: the segment runs from `/v1beta/models/` to the colon before the action. No vendor is ever guessed for a bare model name. |

## Body

`application/json`

Gemini generateContent request, shared by the streamed and the single-response path. The model and the streaming choice come from the path, so the body names neither; the rest of the protocol's fields are forwarded upstream unchanged.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contents` | object[] | Yes | The conversation content, as the protocol defines. Forwarded unchanged. |

## Response

`200` `text/event-stream`

A stream of partial generation results, each in the shape the Gemini protocol defines; the stream is the upstream's, passed through. It is a Server-Sent Events stream with or without `alt=sse`.

## Errors

| Status | Code | Description |
| --- | --- | --- |
| `401` | `invalid_api_key`, `api_key_not_found`, `api_key_revoked`, `api_key_expired` | API key is missing or malformed (`invalid_api_key`), unknown (`api_key_not_found`), revoked (`api_key_revoked`), or expired (`api_key_expired`). |
| `402` | `insufficient_balance` | Wallet balance cannot cover the requested call (`insufficient_balance`). |
| `404` | `model_not_found`, `no_eligible_provider` | Requested model slug is unknown or not enabled (`model_not_found`), or the request's `provider` preferences, merged with the workspace routing settings, leave no provider that serves it (`no_eligible_provider`, with `error.metadata` carrying the routing funnel). |
| `422` | `validation_failed` | The request failed validation (`validation_failed`): the model is not served on this endpoint (the message lists the endpoints it is served on), or a routing field (`provider`, `models`, `route`, `preset`) breaks its rules. The message names the cause. |
| `429` | `rate_limited` | Request rejected by rate limiting (`rate_limited`). |
| `500` | `internal_error` | Other failures: `provider_unavailable` (503) when no healthy upstream can serve the request, `internal_error` (500) for unexpected platform-side failures. |
| `503` | `provider_unavailable` | Other failures: `provider_unavailable` (503) when no healthy upstream can serve the request, `internal_error` (500) for unexpected platform-side failures. |

Error body

```json
{
  "error": {
    "message": "No such model. Model names are vendor-qualified (vendor/model), so a name copied from a provider's own docs needs its vendor prefix added. Call GET /v1/models for the slugs this gateway serves.",
    "type": "not_found_error",
    "code": "model_not_found"
  }
}
```

## Response headers

Each header is listed with the response statuses it is sent with.

| Header | Sent with | Description |
| --- | --- | --- |
| `X-Pi-Trace-Id` | `200`, `401`, `402`, `404`, `422`, `429`, `default` | Platform request id, minted by the gateway. Present on every response of the inference endpoints; quote it when reporting a problem. |
| `Access-Control-Allow-Origin` | `200`, `401`, `402`, `404`, `422`, `429`, `default` | Always `*`. Credentials are the API key header, never a cookie. |
| `Access-Control-Expose-Headers` | `200`, `401`, `402`, `404`, `422`, `429`, `default` | Lists every `X-Pi-*` response header plus `Retry-After` and `Content-Encoding`, so browser code can read them. |
| `X-Pi-Provider` | `200`, `default` | Provider that served this response, in the same vocabulary as `provider.order` and `GET /v1/models/{vendor}/{model}/endpoints`. Present only when the upstream provider answered (a completion or an upstream error); after a fallback it names the provider that took over. |
| `X-Pi-Model` | `200`, `default` | Model slug that served this response - the same value as the body's `model`. When a fallback model took over, it is that model. Present only when the upstream provider answered. |
| `X-Pi-Upstream-Model` | `200`, `default` | The full model id the upstream provider itself reported in its response, passed on verbatim (on a stream, from the first data event). Absent when the upstream response named no model; never filled from the platform's own records. |
| `X-Pi-Upstream-Request-Id` | `200`, `default` | The upstream provider's own request id, passed on verbatim, for looking the call up on the provider's side. Absent when the provider returned none. |

## Request samples

cURL

```bash
curl -N "https://api.pirouter.ai/v1beta/models/google/gemini-3.5-flash:streamGenerateContent" \
  -H "Authorization: Bearer $PIROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [{ "parts": [{ "text": "Hello!" }] }]
  }'
```

Python

```python
import os
from google import genai
from google.genai import types

client = genai.Client(
    api_key=os.environ["PIROUTER_API_KEY"],
    http_options=types.HttpOptions(
        base_url="https://api.pirouter.ai",
        headers={"Authorization": f"Bearer {os.environ['PIROUTER_API_KEY']}"},
    ),
)

for chunk in client.models.generate_content_stream(
    model="google/gemini-3.5-flash",
    contents="Hello!",
):
    print(chunk.text, end="")
```

TypeScript

```ts
import { GoogleGenAI } from '@google/genai'

const ai = new GoogleGenAI({
  apiKey: process.env.PIROUTER_API_KEY,
  httpOptions: {
    baseUrl: 'https://api.pirouter.ai',
    headers: { Authorization: `Bearer ${process.env.PIROUTER_API_KEY}` },
  },
})

const stream = await ai.models.generateContentStream({
  model: 'google/gemini-3.5-flash',
  contents: 'Hello!',
})
for await (const chunk of stream) {
  process.stdout.write(chunk.text ?? '')
}
```

## Response sample

`200` `text/event-stream`

```text
data: {"candidates":[{"content":{"parts":[{"text":"Hi there! How can"}],"role":"model"}}]}

data: {"candidates":[{"content":{"parts":[{"thoughtSignature":"AY89a18/KIDAlSal...","text":""}],"role":"model"}}]}

data: {"candidates":[{"content":{"role":"model"},"finishReason":"STOP"}],"modelVersion":"google/gemini-3.5-flash","responseId":"FqPMarqeLvr16tkPyo3W4QU","usageMetadata":{"promptTokenCount":2,"candidatesTokenCount":10,"thoughtsTokenCount":164,"totalTokenCount":176}}
```
