# Laravel OpenRouter

<br>

[![Latest Version on Packagist](https://img.shields.io/badge/packagist-v2-blue)](https://packagist.org/packages/moe-mizrak/laravel-openrouter) [![OpenRouter Discord](https://img.shields.io/badge/OpenRouter-5865F2?logo=discord\&logoColor=white)](https://discord.gg/3TbKAakhGb) [![Total Downloads](https://img.shields.io/packagist/dt/moe-mizrak/laravel-openrouter.svg?style=flat-square)](https://packagist.org/packages/moe-mizrak/laravel-openrouter)<br>

[![reboosty](https://reboosty-reboosty.vercel.app/api?repo_url=https://github.com/moe-mizrak/laravel-openrouter)](https://reboosty-reboosty.vercel.app/api?repo_url=https://github.com/moe-mizrak/laravel-openrouter)

This Laravel package provides an easy-to-use interface for integrating [**OpenRouter**](https://openrouter.ai/) into your Laravel applications. **OpenRouter** is a unified interface for Large Language Models (LLMs) that allows you to interact with various [**AI models**](https://openrouter.ai/models) through a single API.

## Table of Contents

* [🤖 Requirements](#-requirements)
* [🏁 Get Started](#-get-started)
* [🧩 Configuration](#-configuration)
* [🎨 Usage](#-usage)
  * [Understanding ChatData DTO](#understanding-chatdata-dto)
    * [LLM Parameters](#llm-parameters)
    * [Tool Calling Parameters](#tool-calling-parameters)
    * [Additional Optional Parameters](#additional-optional-parameters)
    * [OpenRouter-only Parameters](#openrouter-only-parameters)
    * [Provider Preferences](#provider-preferences)
  * [Creating a ChatData Instance](#creating-a-chatdata-instance)
  * [Using Facade](#using-facade)
    * [Chat Request](#chat-request)
      * [Stream Chat Request](#stream-chat-request)
      * [Maintaining Conversation Continuity](#maintaining-conversation-continuity)
      * [Tool & Function Calling](#tool--function-calling)
      * [Structured Output](#structured-output)
      * [Web Search](#web-search)
      * [File/Document Inputs](#filedocument-inputs)
      * [Audio Inputs](#audio-inputs)
      * [Prompt caching](#prompt-caching)
    * [Cost Request](#cost-request)
    * [Limit Request](#limit-request)
  * [Using OpenRouterRequest Class](#using-openrouterrequest-class)
* [💫 Contributing](#-contributing)
* [📜 License](#-license)

## 🤖 Requirements

* **PHP 8.2** or **higher**: Use version `v2.x.x` (latest compatible version)
  * Uses [spatie laravel-data](https://github.com/spatie/laravel-data)
* **PHP 8.1**: Use version `v1.0.9`.
  * Uses [spatie data-transfer-object](https://github.com/spatie/data-transfer-object)
  * For correct way to use DTOs, refer to [v1.0.9 README](https://github.com/moe-mizrak/laravel-openrouter/tree/v1.0.9).

## 🏁 Get Started

You can **install** the package via composer:

```bash
composer require moe-mizrak/laravel-openrouter
```

You can **publish** the **config file** with:

```bash
php artisan vendor:publish --tag=laravel-openrouter
```

This is the contents of the **published config file**:

```php
return [
    'api_endpoint' => env('OPENROUTER_API_ENDPOINT', 'https://openrouter.ai/api/v1/'),
    'api_key'      => env('OPENROUTER_API_KEY'),
    'api_timeout'  => env('OPENROUTER_API_TIMEOUT', 20),
    'title'        => env('OPENROUTER_API_TITLE', 'laravel-openrouter'),
    'referer'      => env('OPENROUTER_API_REFERER', 'https://github.com/moe-mizrak/laravel-openrouter'),
];
```

## 🧩 Configuration

After publishing the package configuration file, you'll need to add the following environment variables to your **.env** file:

```env
OPENROUTER_API_ENDPOINT=https://openrouter.ai/api/v1/
OPENROUTER_API_KEY=your_api_key
OPENROUTER_API_TIMEOUT=request_timeout
OPENROUTER_API_TITLE=
OPENROUTER_API_REFERER=
```

> \[!NOTE]
>
> * `OPENROUTER_API_ENDPOINT`: The endpoint URL for the **OpenRouter API** (default: <https://openrouter.ai/api/v1/>).
> * `OPENROUTER_API_KEY`: Your **API key** for accessing the OpenRouter API. You can obtain this key from the [OpenRouter dashboard](https://openrouter.ai/settings/keys).
> * `OPENROUTER_API_TIMEOUT`: Request timeout in seconds. Increase value to 120 - 180 if you use long-thinking models like openai/o1 (default: 20)
> * `OPENROUTER_API_TITLE`: Optional - Site URL for rankings on openrouter.ai
> * `OPENROUTER_API_REFERER`: Optional - Site referer for rankings on openrouter.ai

## 🎨 Usage

This package provides two ways to interact with the OpenRouter API:

* Using the [`LaravelOpenRouter`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/Facades/LaravelOpenRouter.php) facade
* Instantiating the [`OpenRouterRequest`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/OpenRouterRequest.php) class directly.

Both methods utilize the [`ChatData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ChatData.php) DTO class to structure the data sent to the API.

### Understanding ChatData DTO

The [`ChatData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ChatData.php) class is used to **encapsulate the data** required for making chat requests to the OpenRouter API. Here's a breakdown of the key properties:

* **messages** (array|null): An array of [`MessageData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/MessageData.php) objects representing the chat messages. This field is XOR-gated with the `prompt` field.
* **prompt** (string|null): A string representing the prompt for the chat request. This field is XOR-gated with the `messages` field.
* **model** (string|null): The name of the model to be used for the chat request. If not specified, the user's default model will be used. This field is XOR-gated with the `models` field.
* **response\_format** (ResponseFormatData|null): An instance of the [`ResponseFormatData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ResponseFormatData.php) class representing the desired format for the response.
* **usage** (bool|null): A boolean indicating whether to include usage information in the response. Default is `false` because enabling usage accounting will add a few hundred milliseconds to the response as the API calculates token counts and costs.
* **stop** (array|string|null): A value specifying the stop sequence for the chat generation.
* **stream** (bool|null): A boolean indicating whether streaming should be enabled or not.
* **include\_reasoning** (bool|null): Whether to return the model's reasoning (Note: this parameter is **deprecated**, use `reasoning` parameter instead. For backward compatibility, package still supports the `include_reasoning` parameter)
* **reasoning** (ReasoningData|null): An instance of the [`ReasoningData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ReasoningData.php) class for reasoning configuration. It provides a transparent look into the reasoning steps taken by a model.
* **cache\_control** ([`CacheControlData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/CacheControlData.php)|null): Controls **prompt caching** on supported providers/models. You can set it at the **top-level** of the request (recommended for multi-turn conversations) or as an explicit **breakpoint** on large text blocks via `TextContentData::$cache_control`. For details and provider-specific behavior, see [OpenRouter Prompt Caching](https://openrouter.ai/docs/guides/best-practices/prompt-caching).

#### LLM Parameters

These properties control various aspects of the generated response (more [info](https://openrouter.ai/docs/api-reference/parameters)):

* **max\_tokens** (int|null): The maximum number of tokens that can be generated in the completion. Default is 1024.
* **temperature** (float|null): A value between 0 and 2 controlling the randomness of the output.
* **top\_p** (float|null): A value between 0 and 1 for nucleus sampling, an alternative to temperature sampling.
* **top\_k** (float|null): A value between 1 and infinity for top-k sampling (not available for OpenAI models).
* **frequency\_penalty** (float|null): A value between -2 and 2 for penalizing new tokens based on their existing frequency.
* **presence\_penalty** (float|null): A value between -2 and 2 for penalizing new tokens based on whether they appear in the text so far.
* **repetition\_penalty** (float|null): A value between 0 and 2 for penalizing repetitive tokens.
* **seed** (int|null): A value for deterministic sampling (OpenAI models only, in beta).

#### Tool-calling Parameters

Only natively suported by OpenAI models. For others, we submit a YAML-formatted string with these tools at the end of the prompt.

* **tool\_choice** (string|array|null): A value specifying the tool choice for function calling (OpenAI models only).
* **tools** (array|null): An array of [`ToolCallData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ToolCallData.php) objects for function calling.

#### Additional optional parameters

* **logit\_bias** (array|null): An array for modifying the likelihood of specified tokens appearing in the completion.

#### OpenRouter-only parameters

* **transforms** (array|null): An array for configuring prompt transforms.
* **plugins** (array|null): An array of [`PluginData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/PluginData.php) objects for configuring plugins such as `web search` or `file parsing`.
* **web\_search\_options** (WebSearchOptionsData|null): An instance of the [`WebSearchOptionsData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/WebSearchOptionsData.php) DTO object for configuring web search (e.g. `search_context_size: SearchContextSizeType::LOW`).
* **models** (array|null): An array of models to automatically try if the primary model is unavailable. This field is XOR-gated with the `model` field.
* **route** (string|null): A value specifying the route type (e.g., `RouteType::FALLBACK`).
* **provider** (ProviderPreferencesData|null): An instance of the [`ProviderPreferencesData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ProviderPreferencesData.php) DTO object for configuring provider preferences (see [Provider Preferences](#provider-preferences)).

#### Provider Preferences

The [`ProviderPreferencesData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ProviderPreferencesData.php) DTO allows fine-grained control over provider routing. For more details, see [OpenRouter Provider Selection](https://openrouter.ai/docs/guides/routing/provider-selection).

| Property                   | Type                             | Description                                                                                                                            |
| -------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `allow_fallbacks`          | `bool\|null`                     | Whether to allow backup providers when the primary is unavailable (default: `true`).                                                   |
| `require_parameters`       | `bool\|null`                     | Only use providers that support all parameters in your request.                                                                        |
| `data_collection`          | `string\|null`                   | `DataCollectionType::ALLOW` (default) or `DataCollectionType::DENY`.                                                                   |
| `order`                    | `array\|null`                    | Ordered list of provider slugs to prefer.                                                                                              |
| `zdr`                      | `bool\|null`                     | Restrict routing to only Zero Data Retention endpoints.                                                                                |
| `enforce_distillable_text` | `bool\|null`                     | Restrict routing to only models that allow text distillation.                                                                          |
| `only`                     | `array\|null`                    | List of provider slugs to allow for this request.                                                                                      |
| `ignore`                   | `array\|null`                    | List of provider slugs to skip for this request.                                                                                       |
| `quantizations`            | `array\|null`                    | Filter by quantization levels (see `QuantizationType`).                                                                                |
| `sort`                     | `string\|ProviderSortData\|null` | Sort providers by `"price"`, `"throughput"`, or `"latency"`. Can also be a `ProviderSortData` object with `by` and `partition` fields. |
| `preferred_min_throughput` | `float\|PercentileData\|null`    | Preferred minimum throughput (tokens/sec). Can be a number or `PercentileData` with percentile cutoffs (`p50`, `p75`, `p90`, `p99`).   |
| `preferred_max_latency`    | `float\|PercentileData\|null`    | Preferred maximum latency (seconds). Can be a number or `PercentileData` with percentile cutoffs.                                      |
| `max_price`                | `MaxPriceData\|null`             | Maximum acceptable pricing with `prompt`, `completion`, `request`, and `image` fields (in USD).                                        |

**Example with advanced provider preferences:**

```php
$provider = new ProviderPreferencesData(
    allow_fallbacks: true,
    require_parameters: true,
    data_collection: DataCollectionType::DENY,
    only: ['openai', 'anthropic'],
    quantizations: [QuantizationType::FP16, QuantizationType::BF16],
    sort: new ProviderSortData(
        by: ProviderSortType::PRICE,
        partition: true,
    ),
    preferred_min_throughput: new PercentileData(
        p50: 100.0,
        p90: 50.0,
    ),
    preferred_max_latency: 2.5,
    max_price: new MaxPriceData(
        prompt: 0.001,
        completion: 0.002,
    ),
);

$chatData = new ChatData(
    messages: [
        new MessageData(
            role: RoleType::USER,
            content: 'Hello!',
        ),
    ],
    model: 'openai/gpt-4o',
    provider: $provider,
);
```

### Creating a ChatData Instance

This is a sample chat data instance (Refer to [spatie laravel-data](https://spatie.be/docs/laravel-data/v4/introduction) how to create, use DTOs):

```php
$chatData = new ChatData(
    messages: [
        new MessageData(
            role: RoleType::USER,
            content: [
                new TextContentData(
                    type: TextContentData::ALLOWED_TYPE,
                    text: 'This is a sample text content.',
                ),
                new ImageContentPartData(
                    type: ImageContentPartData::ALLOWED_TYPE,
                    image_url: new ImageUrlData(
                        url: 'https://example.com/image.jpg',
                        detail: 'Sample image',
                    ),
                ),
            ],
        ),
    ],
    response_format: new ResponseFormatData(
        type: 'json_object',
    ),
    usage: true,
    stop: ['stop_token'],
    stream: true,
    reasoning: new ReasoningData(
        effort: EffortType::HIGH,
        exclude: false,
    ),
    max_tokens: 1024,
    temperature: 0.7,
    top_p: 0.9,
    top_k: 50,
    frequency_penalty: 0.5,
    presence_penalty: 0.2,
    repetition_penalty: 1.2,
    seed: 42,
    tool_choice: 'auto',
    tools: [
        // ToolCallData instances
    ],
    logit_bias: [
        '50256' => -100,
    ],
    transforms: ['middle-out'],
    plugins: [
        new PluginData(
            id: 'web',
            max_results: 3,
            engine: 'native',
        ),
    ],
    web_search_options: new WebSearchOptionsData(
        search_context_size: SearchContextSizeType::MEDIUM,
    ),
    models: ['model1', 'model2'],
    route: RouteType::FALLBACK,
    provider: new ProviderPreferencesData(
        allow_fallbacks: true,
        require_parameters: true,
        data_collection: DataCollectionType::ALLOW,
        ignore: ['anthropic'],
        quantizations: [QuantizationType::FP16, QuantizationType::BF16],
        sort: ProviderSortType::PRICE,
        max_price: new MaxPriceData(
            prompt: 0.001,
            completion: 0.002,
        ),
    ),
    modalities: ['image', 'text'],
    image_config: new ImageConfigData(
        aspect_ratio: '16:9',
    ),
);
```

### Using Facade

The `LaravelOpenRouter` facade offers a convenient way to make OpenRouter API requests.

#### Chat Request

To send a chat request, create an instance of [`ChatData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ChatData.php) and pass it to the `chatRequest` method:

```php
$content = 'Tell me a story about a rogue AI that falls in love with its creator.'; // Your desired prompt or content
$model = 'mistralai/mistral-7b-instruct:free'; // The OpenRouter model you want to use (https://openrouter.ai/models)
$messageData = new MessageData(
    content: $content,
    role: RoleType::USER,
);

$chatData = new ChatData(
    messages: [
        $messageData,
    ],
    model: $model,
    max_tokens: 100, // Adjust this value as needed
);

$chatResponse = LaravelOpenRouter::chatRequest($chatData);

// You can convert the response `toArray` if needed (It converts ResponseData DTO to array including the nested DTOs while filtering null values)
$responseArray = $chatResponse->toArray();
```

* **Stream Chat Request**

  Streaming chat request is also supported and can be used as following by using **chatStreamRequest** function:

```php
$content = 'Tell me a story about a rogue AI that falls in love with its creator.'; // Your desired prompt or content
$model = 'mistralai/mistral-7b-instruct:free'; // The OpenRouter model you want to use (https://openrouter.ai/models)
$messageData = new MessageData(
    content: $content,
    role: RoleType::USER,
);

$chatData = new ChatData(
    messages: [
        $messageData,
    ],
    model: $model,
    max_tokens: 100,
);

/*
 * Calls chatStreamRequest ($promise is type of PromiseInterface)
 */
$promise = LaravelOpenRouter::chatStreamRequest($chatData);

// Waits until the promise completes if possible.
$stream = $promise->wait(); // $stream is type of GuzzleHttp\Psr7\Stream

/*
 * 1) You can retrieve whole raw response as: - Choose 1) or 2) depending on your case.
 */
$rawResponseAll = $stream->getContents(); // Instead of chunking streamed response as below - while (! $stream->eof()), it waits and gets raw response all together.
$response = LaravelOpenRouter::filterStreamingResponse($rawResponseAll); // Optionally you can use filterStreamingResponse to filter raw streamed response, and map it into array of responseData DTO same as chatRequest response format.

// 2) Or Retrieve streamed raw response as it becomes available:
while (! $stream->eof()) {
    $rawResponse = $stream->read(1024); // readByte can be set as desired, for better performance 4096 byte (4kB) can be used.

    /*
     * Optionally you can use filterStreamingResponse to filter raw streamed response, and map it into array of responseData DTO same as chatRequest response format.
     */
    $response = LaravelOpenRouter::filterStreamingResponse($rawResponse);
}
```

You do **not** need to specify `'stream' = true` in ChatData since `chatStreamRequest` does it for you.

<details>

<summary>Streaming usage example:</summary>

```php
Route::get('/test/stream', function () {
    $content = 'Tell me a story about a rogue AI that falls in love with its creator.';
    $model = 'deepseek/deepseek-chat-v3.1';
    
    $messageData = new MessageData(
        content: $content,
        role: RoleType::USER,
    );
    
    $chatData = new ChatData(
        messages: [$messageData],
        model: $model,
        max_tokens: 1000,
    );
    
    return response()->stream(function () use ($chatData) {
        $promise = LaravelOpenRouter::chatStreamRequest($chatData);
        $stream = $promise->wait();
        
        while (!$stream->eof()) {
            $rawResponse = $stream->read(1024);
            
            // 1) Print raw streamed response as it becomes available:
            echo $rawResponse;
            
            /*
             * 2) Optionally you can use filterStreamingResponse to filter raw streamed response, and map it into array of responseData DTO same as chatRequest response format.
             */
            $response = LaravelOpenRouter::filterStreamingResponse($rawResponse);
            
            foreach ($response as $responseData) {
                // Process each responseData as needed
                echo json_encode($responseData->toArray()) . PHP_EOL;
            }
            
            flush();
        }
    }, 200, [
        'Content-Type' => 'text/plain; charset=utf-8',
    ]);
});
```

</details>

<details>

<summary>Sample rawResponse:</summary>

This is the expected sample rawResponse (raw response returned from OpenRouter stream chunk) `$rawResponse`:

```php
"""
: OPENROUTER PROCESSING\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":"Title"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":": Quant"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":"um Echo"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":": A Sym"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGG
"""

"""
IsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":"phony of Code"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":"\n\nIn"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":" the heart of"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":" the bustling"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistra
"""

"""
l-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":" city of Ne"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":"o-Tok"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":"yo, a"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718885921,"choices":[{"index":0,"delta":{"role":"assistant","content":" brilliant young research"},"finish_reason":null}]}\n
\n
data: {"id":"gen-eWgGaEbIzFq4ziGGIsIjyRtLda54","model":"mistralai/mistral-7b-instruct:free","object":"chat.com
"""
...

: OPENROUTER PROCESSING\n
\n
data: {"id":"gen-C6Xym94jZcvJv2vVpxYSyw2tV1fR","model":"mistralai/mistral-7b-instruct:free","object":"chat.completion.chunk","created":1718887189,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"usage":{"prompt_tokens":23,"completion_tokens":100,"total_tokens":123,"cost":0.00000114}}\n
\n
data: [DONE]\n
```

Last `data:` carries usage information of streaming. `data: [DONE]\n` returned from OpenRouter server when streaming is over.

This is the sample response after filterStreamingResponse:

```
[
    ResponseData(
        id: "gen-QcWgjEtiEDNHgomV2jjoQpCZlkRZ",
        model: "mistralai/mistral-7b-instruct:free",
        object: "chat.completion.chunk",
        created: 1718888436,
        choices: [
            [
                "index" => 0,
                "delta" => [
                    "role" => "assistant",
                    "content" => "Title"
                ],
                "finish_reason" => null
            ]
        ],
        usage: null
    ),
    ResponseData(
        id: "gen-QcWgjEtiEDNHgomV2jjoQpCZlkRZ",
        model: "mistralai/mistral-7b-instruct:free",
        object: "chat.completion.chunk",
        created: 1718888436,
        choices: [
            [
                "index" => 0,
                "delta" => [
                    "role" => "assistant",
                    "content" => "Quant"
                ],
                "finish_reason" => null
            ]
        ],
        usage: null
    ),
    ...
    ResponseData(
        id: 'gen-QcWgjEtiEDNHgomV2jjoQpCZlkRZ',
        model: 'mistralai/mistral-7b-instruct:free',
        object: 'chat.completion.chunk',
        created: 1718888436,
        choices: [
            [
                'index' => 0,
                'delta' => [
                    'role' => 'assistant',
                    'content' => '',
                ],
                'finish_reason' => null,
            ],
        ],
        usage: new UsageData(
            prompt_tokens: 23,
            completion_tokens: 100,
            total_tokens: 123,
            cost: 0.00000114,
            prompt_tokens_details: new PromptTokensDetailsData(
                cached_tokens: 10,
                cache_write_tokens: 15,
                audio_tokens: 0,
                video_tokens: 0,
            ),
            completion_tokens_details: new CompletionTokensDetailsData(
                reasoning_tokens: 30,
                audio_tokens: 0,
                image_tokens: 0,
                accepted_prediction_tokens: 90,
                rejected_prediction_tokens: 0,
            ),
        ),
    ),
]
```

</details>

* **Maintaining Conversation Continuity**

  If you want to maintain **conversation continuity** meaning that historical chat will be remembered and considered for your new chat request, you need to send historical messages along with the new message:

```php
$model = 'mistralai/mistral-7b-instruct:free';

$firstMessage = new MessageData(
    role: RoleType::USER,
    content: 'My name is Moe, the AI necromancer.',
);

$chatData = new ChatData(
    messages: [
        $firstMessage,
    ],
    model: $model,
);
// This is the chat which you want LLM to remember
$oldResponse = LaravelOpenRouter::chatRequest($chatData);

/*
* You can skip part above and just create your historical message below (maybe you retrieve historical messages from DB etc.)
*/

// Here adding historical response to new message
$historicalMessage = new MessageData(
    role: RoleType::ASSISTANT, // Set as assistant since it is a historical message retrieved previously
    content: Arr::get($oldResponse->choices[0], 'message.content'), // Historical response content retrieved from previous chat request
);
// This is your new message
$newMessage = new MessageData(
    role: RoleType::USER,
    content: 'Who am I?',
);

$chatData = new ChatData(
    messages: [
        $historicalMessage,
        $newMessage,
    ],
    model: $model,
);

$response = LaravelOpenRouter::chatRequest($chatData);
```

Expected response:

```php
$content = Arr::get($response->choices[0], 'message.content');
// content = You are Moe, a fictional character and AI Necromancer, as per the context of the conversation we've established. In reality, you are the user interacting with me, an assistant designed to help answer questions and engage in friendly conversation.
```

* **Tool & Function Calling**

  (Please also refer to [OpenRouter Document Function Calling](https://openrouter.ai/docs/guides/features/tool-calling) for more details and [models supporting tool calling](https://openrouter.ai/models?supported_parameters=tools))

Tool calls (also known as function calls) give an LLM access to external tools. The LLM does not call the tools directly. Instead, it suggests the tool to call.

> \[!NOTE] The user then calls the tool separately and provides the results back to the LLM. Finally, the LLM formats the response into an answer to the user’s original question.

This is an example of how to use tool calling with OpenRouter:

```php
// Define the tool using ToolCallData and FunctionData
$tools = [
    new ToolCallData(
        type: 'function',
        function: new FunctionData(
            name: 'getWeather',
            description: 'Get the current weather for a location',
            parameters: [
                'type' => 'object',
                'properties' => [
                    'location' => [
                        'type' => 'string',
                        'description' => 'The city name',
                    ],
                ],
                'required' => ['location'],
            ],
        ),
    ),
];

// Create chat request with tools
$chatData = new ChatData(
    messages: [
        new MessageData(
            role: RoleType::USER,
            content: 'What is the weather like in Tokyo?',
        ),
    ],
    model: 'openai/gpt-4o-mini',
    tools: $tools,
    tool_choice: 'auto',
);

// Send request, if tool call is suggested by LLM, call the tool and provide the result back to LLM as below
$response = LaravelOpenRouter::chatRequest($chatData);

// If LLM suggested tool call then extract the tool call returned from LLM
$toolCall = $response['choices'][0]['message']['tool_calls'][0] ?? null;
// the model/LLM generated ID
$toolCallId = $toolCall['id']; // e.g. "call_7F3kP9"

// Sample tool result assuming you called the tool and got the result
$toolResult = [
    'temperature' => '22°C',
    'condition'   => 'Sunny',
];

// Provide tool result back to LLM:

// The original user message
$userMessage = new MessageData(
    role: RoleType::USER,
    content: 'What is the weather like in Tokyo?',
);

// Tool call assistant message with tool call ID
$assistantToolCallMessage = new MessageData(
    role: RoleType::ASSISTANT,
    tool_calls: [
        new ToolCallData(
            id: $toolCallId,
            type: 'function',
            function: new FunctionCallData(
                name: $toolCall['function']['name'],
                arguments: $toolCall['function']['arguments'],
            ),
        ),
    ],
);

// Tool response message with tool result and tool call ID
$toolResponseMessage = new MessageData(
    role: RoleType::TOOL,
    tool_call_id: $toolCallId,
    content: json_encode($toolResult),
);

$chatDataWithToolResult = new ChatData(
    messages: [
        $userMessage,
        $assistantToolCallMessage,
        $toolResponseMessage,
    ],
    model: 'openai/gpt-4o-mini',
);

// Send request with tool result
$finalResponse = LaravelOpenRouter::chatRequest($chatDataWithToolResult);
```

> Basically it follows these steps:
>
> 1. Define the tools and send the initial chat request including the tools and tool\_choice optional parameters.
> 2. If the LLM suggests a tool call, extract the tool call information from the response and make the actual tool call separately.
> 3. Provide the tool result back to the LLM by creating a new chat request that includes the original user message, the assistant's tool call message, and the tool response message including the tool\_call\_id which is the ID generated by the model/LLM for the tool call.
> 4. Send the new chat request to get the final response from the LLM.

* **Structured Output**

  (Please also refer to [OpenRouter Document Structured Output](https://openrouter.ai/docs/features/structured-outputs) for models supporting structured output, also for more details)

If you want to receive the response in a structured format, you can specify the `type` property for `response_format` ([ResponseFormatData](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ResponseFormatData.php)) as `json_object` in the [`ChatData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ChatData.php) object.

Additionally, it's recommended to set the `require_parameters` property for `provider` ([ProviderPreferencesData](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ProviderPreferencesData.php)) to `true` in the [`ChatData`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/DTO/ChatData.php) object.

> \[!CAUTION] When using structured outputs, you may encounter these scenarios:
>
> * Model doesn’t support structured outputs
> * Invalid schema
>
> Also: If you face an error, remove `require_parameters` property of `provider` to see the result.
>
> Check out [Requiring Providers to Support All Parameters](https://openrouter.ai/docs/features/provider-routing#requiring-providers-to-support-all-parameters-beta) for more details.

```php
$chatData = new ChatData(
    messages: [
        new MessageData(
            role: RoleType::USER,
            content: 'Tell me a story about a rogue AI that falls in love with its creator.',
        ),
    ],
    model: 'mistralai/mistral-7b-instruct:free',
    response_format: new ResponseFormatData(
        type: 'json_object',
    ),
    provider: new ProviderPreferencesData(
        require_parameters: true,
    ),
);
```

You can also specify the `response_format` as `json_schema` to receive the response in a specified schema format (Advisable to set `'strict' => true` in `json_schema` array for strict schema):

```php
$chatData = new ChatData(
    messages: [
        new MessageData(
            role   : RoleType::USER,
            content: 'Tell me a story about a rogue AI that falls in love with its creator.',
        ),
    ],
    model: 'mistralai/mistral-7b-instruct:free',
    response_format: new ResponseFormatData(
        type: 'json_schema',
        json_schema: [
            'name' => 'article',
            'strict' => true,
            'schema' => [
                'type' => 'object',
                'properties' => [
                    'title' => [
                        'type' => 'string',
                        'description' => 'article title'
                    ],
                    'details' => [
                        'type' => 'string',
                        'description' => 'article detail'
                    ],
                    'keywords' => [
                        'type' => 'string',
                        'description' => 'article keywords',
                    ],
                ],
                'required' => ['title', 'details', 'keywords'],
                'additionalProperties' => false
            ]
        ],
    ),
    provider: new ProviderPreferencesData(
        require_parameters: true,
    ),
);
```

> \[!TIP] You can also use **prompt engineering** to obtain structured output and control the format of responses.

* **Web Search**

  (Please also refer to [OpenRouter Document Web Search](https://openrouter.ai/docs/guides/features/plugins/web-search) for more details)

**Web Search** feature works on any model on OpenRouter.

You can incorporate relevant web search results for any model on OpenRouter by activating and customizing the web plugin, or by appending :online to the model slug. e.g., `model: "openai/gpt-4o:online"` or `model: "openai/gpt-oss-20b:free:online"`.

```php
$chatData = new ChatData(
    messages: [
        new MessageData(
            role: RoleType::USER,
            content: 'What are the latest developments in AI?',
        ),
    ],
    model: 'openai/gpt-4o:online',
    web_search_options: new WebSearchOptionsData(
        search_context_size: SearchContextSizeType::HIGH, // Optional: low, medium, high
    ),
);
```

`web_search_options` parameter in ChatData is optional, where you can customize the web search behavior as `search_context_size` can be set to `low`, `medium`, or `high` depending on how much web search context you want to include in the response.

* `SearchContextSizeType::LOW` is for minimal search context, suitable for basic queries.
* `SearchContextSizeType::MEDIUM` is for moderate amount of web search context.
* `SearchContextSizeType::HIGH` is for extensive web search context.

`:online` is a shortcut for using the web plugin, and is exactly equivalent to:

```php
$chatData = new ChatData(
    messages: [
        new MessageData(
            role: RoleType::USER,
            content: 'What are the latest developments in AI?',
        ),
    ],
    model: 'openai/gpt-4o',
    plugins: [
        new PluginData(
            id: 'web',
            max_results: 5, // Optional: number of search results to retrieve
            engine: 'undefined', // Optional: "native", "exa", or "undefined"
        ),
    ],
    web_search_options: new WebSearchOptionsData(
        search_context_size: SearchContextSizeType::MEDIUM, // Optional: low, medium, high
    ),
);
```

`engine` parameter in PluginData is optional, where you can specify the web search engine to be used:

* `native`: Always uses the model provider’s built-in web search capabilities
* `exa`: Uses Exa’s search API for web results
* `undefined`: Uses native search if available for the provider, otherwise falls back to Exa

And here is how **annotations** are included in the response when using **web search** where annotations contains `type`: `url_citation` and `url_citation`: {`url`, `title`, `content`, `start_index`, `end_index`}:

```php
$response = LaravelOpenRouter::chatRequest($chatData);

$annotations = Arr::get($response->choices[0], 'message.annotations', []);
```

* **File/Document Inputs**

  (Please also refer to [OpenRouter Document File Inputs](https://openrouter.ai/docs/guides/overview/multimodal/pdfs) for more details)

You can provide file input by using the `FileContentData` DTO class as following:

```php
$model = 'anthropic/claude-3.5-sonnet';

// Plugin configuration for file parsing, optional
$plugins = [
    new PluginData(
        id: 'file-parser',
        pdf: [
            'engine' => 'pdf-text', // Supported engines: pdf-text, mistral-ocr and native
        ],
    ),
];

// For the publicly accessible PDFs
$fileContentData = new FileContentData(
    type: FileContentData::ALLOWED_TYPE,
    file: new FileUrlData(
        file_data: 'https://example.com/report.pdf',
        filename: 'quarterly-report.pdf',
    ),
);

$textContentData = new TextContentData(
    type: TextContentData::ALLOWED_TYPE,
    text: 'Please summarize this document.',
);

$messageData = new MessageData(
    content: [
        $textContentData,
        $fileContentData,
    ],
    role: RoleType::USER,
);

$chatData = new ChatData(
    messages: [$messageData],
    model: $model,
    plugins: $plugins,
);

$response = LaravelOpenRouter::chatRequest($chatData);
```

> \[!NOTE]
>
> * `plugins` parameter in ChatData is optional.
> * If you don’t explicitly specify an **engine**, OpenRouter will default first to the model’s native file processing capabilities, and if that’s not available, it will use the "mistral-ocr" engine.

*Using base64-encoded files*:

```php
$base64Data = base64_encode(file_get_contents('/path/to/document.pdf'));

$fileContentData = new FileContentData(
    type: FileContentData::ALLOWED_TYPE,
    file: new FileUrlData(
        file_data: "data:application/pdf;base64,{$base64Data}",
        filename: 'document.pdf',
    ),
);
```

> \[!TIP]
>
> * **File input** feature works on any model on OpenRouter.
> * The `filename` parameter is optional but recommended for context.

* **Audio Inputs**

  (Please also refer to [OpenRouter Document Audio Inputs](https://openrouter.ai/docs/features/multimodal/audio) for models supporting audio inputs, also for more details)

Audio input is supported by some models in OpenRouter. You can provide audio input by using the `AudioContentData` DTO class as following:

```php
$model = 'mistralai/voxtral-small-24b-2507'; // Audio input supported models: https://openrouter.ai/models?fmt=cards&input_modalities=audio
$data = base64_encode('path/of/audio/file.mp3'); // Base64-encoded audio data

$audioContentData = new AudioContentData(
    type: AudioContentData::ALLOWED_TYPE, // it can only take input_audio for audio content
    input_audio: new InputAudioData(
        data: $data,
        format: AudioFormatType::MP3, // Supported formats: mp3, wav
    ),
);

$textContentData = new TextContentData(
    type: TextContentData::ALLOWED_TYPE,
    text: 'Please transcribe this audio file.',
);

$messageData = new MessageData(
    content: [
        $textContentData,
        $audioContentData,
    ],
    role: RoleType::USER,
);

$chatData = new ChatData(
    messages: [
        $messageData,
    ],
    model: $model,
);

$response = LaravelOpenRouter::chatRequest($chatData);
```

> \[!NOTE] Only `mp3` and `wav` formats are supported for audio inputs.
>
> And make sure to provide valid `base64-encoded` audio data.

* **Prompt caching**

OpenRouter supports prompt caching on supported providers/models to decrease cost and latency on repeated requests. This package supports both approaches described in OpenRouter docs:

* **Top-level caching** (recommended for multi-turn conversations): set `ChatData::$cache_control`.
* **Explicit cache breakpoint** (fine-grained control): set `TextContentData::$cache_control` on the *large* text block(s) you want to cache (e.g. RAG data, CSV data, long instructions, etc.).

OpenRouter docs: [Prompt Caching](https://openrouter.ai/docs/guides/best-practices/prompt-caching)

**Top-level example (automatic caching):**

```php
use MoeMizrak\LaravelOpenrouter\DTO\CacheControlData;
use MoeMizrak\LaravelOpenrouter\DTO\ChatData;
use MoeMizrak\LaravelOpenrouter\DTO\MessageData;
use MoeMizrak\LaravelOpenrouter\Types\RoleType;

$chatData = new ChatData(
    messages: [
        new MessageData(
            role: RoleType::USER,
            content: 'What triggered the collapse?',
        ),
    ],
    model: 'anthropic/claude-sonnet-4.6',
    cache_control: new CacheControlData(
        type: CacheControlData::ALLOWED_TYPE, // "ephemeral"
        ttl: '1h', // optional
    ),
);
```

**Explicit breakpoint example (cache a large text block):**

```php
use MoeMizrak\LaravelOpenrouter\DTO\CacheControlData;
use MoeMizrak\LaravelOpenrouter\DTO\ChatData;
use MoeMizrak\LaravelOpenrouter\DTO\MessageData;
use MoeMizrak\LaravelOpenrouter\DTO\TextContentData;
use MoeMizrak\LaravelOpenrouter\Types\RoleType;

$chatData = new ChatData(
    messages: [
        new MessageData(
            role: RoleType::USER,
            content: [
                new TextContentData(
                    text: 'Given the book below:',
                ),
                new TextContentData(
                    text: 'HUGE TEXT BODY',
                    cache_control: new CacheControlData(
                        type: CacheControlData::ALLOWED_TYPE, // "ephemeral"
                        ttl: '1h', // optional
                    ),
                ),
                new TextContentData(
                    text: 'What triggered the collapse?',
                ),
            ],
        ),
    ],
    model: 'anthropic/claude-sonnet-4.6',
);
```

#### Cost Request

To retrieve the cost of a generation, first make a `chat request` and obtain the `generationId`. Then, pass the generationId to the `costRequest` method:

```php
$content = 'Tell me a story about a rogue AI that falls in love with its creator.'; // Your desired prompt or content
$model = 'mistralai/mistral-7b-instruct:free'; // The OpenRouter model you want to use (https://openrouter.ai/models)
$messageData = new MessageData(
    content: $content,
    role   : RoleType::USER,
);

$chatData = new ChatData(
    messages: [
        $messageData,
    ],
    model: $model,
    max_tokens: 100,
);

$chatResponse = LaravelOpenRouter::chatRequest($chatData);
$generationId = $chatResponse->id; // generation id which will be passed to costRequest

$costResponse = LaravelOpenRouter::costRequest($generationId);

// You can convert the response `toArray` if needed (It converts CostResponseData DTO to array while filtering null values)
$responseArray = $costResponse->toArray();
```

#### Limit Request

To retrieve rate limit and credits left on the API key:

```php
$limitResponse = LaravelOpenRouter::limitRequest();

// You can convert the response `toArray` if needed (It converts LimitResponseData DTO to array including the nested DTOs while filtering null values)
$responseArray = $limitResponse->toArray();
```

### Using OpenRouterRequest Class

You can also inject the [`OpenRouterRequest`](https://github.com/moe-mizrak/laravel-openrouter/blob/master/src/OpenRouterRequest.php) class in the **constructor** of your class and use its methods directly.

```php
public function __construct(protected OpenRouterRequest $openRouterRequest) {}

/*
 * Similarly, you can use OpenRouterRequest class methods as below:
 */
// Chat Request
$response = $this->openRouterRequest->chatRequest($chatData);

// Stream Chat Request
$streamResponse = $this->openRouterRequest->chatStreamRequest($chatData);

// Cost Request
$costResponse = $this->openRouterRequest->costRequest($generationId);

// Limit Request
$limitResponse = $this->openRouterRequest->limitRequest();
```

## 💫 Contributing

> **We welcome contributions!** If you'd like to improve this package, simply create a pull request with your changes. Your efforts help enhance its functionality and documentation.

Please read our [**Contributing Guide**](https://moe-mizrak.gitbook.io/laravel-openrouter/contributing) for details on our development process, commit conventions, and how to submit pull requests.

## 📜 License

Laravel OpenRouter is an open-sourced software licensed under the [**MIT license**](https://github.com/moe-mizrak/laravel-openrouter/blob/master/LICENSE/README.md).
