# OpenRouter for Agents - Tools & Fallbacks (`automa-flow/openrouter-agent-gateway`) Actor

Call OpenRouter with full chat messages, custom tools, validated JSON and model fallbacks. No provider keys. Gateway: $0.01/run + $0.003 per usable response; model usage is billed separately.

- **URL**: https://apify.com/automa-flow/openrouter-agent-gateway.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** AI, Agents, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 successful agent calls

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## OpenRouter for Agents - Tools & Fallbacks

Call OpenRouter models from AI agents using full chat messages, custom tools, structured JSON and model fallbacks - no separate provider API keys.

[Release notes](https://apify.com/automa-flow/openrouter-agent-gateway/changelog)

This Actor is a bounded OpenAI-style LLM invocation primitive for n8n, Make, backend automations and other Apify Actors. It is not a generic agent framework and it does not execute tools.

### What this Actor solves

Agent workflows need a reliable chat-completions call with:

- the original `messages[]` (not a collapsed prompt);
- caller-defined `tools[]`;
- structured JSON (`json_object` / `json_schema`);
- OpenRouter native `models[]` fallback;
- one Dataset row per request, with explicit SUCCESS / FAILED / PARTIAL.

Authentication is the Apify runtime token. You do not need an OpenRouter account or provider keys. Model usage is billed to your Apify account through the official `apify/openrouter` proxy; this Actor charges a small orchestration event on top.

### Why use it instead of a basic OpenRouter wrapper

| Alternative | What it does | Gap |
| --- | --- | --- |
| `apify/openrouter` | Standby OpenAI-compatible proxy | Only callable as `APIFY_ACTOR`; no Dataset batch contract |
| `watchful_yotar/openrouter-wrapper` | Single `prompt` string | No `messages[]`, no custom tools, no fallback chain |
| `fayoussef/bulk-llm-runner` | Bulk prompt / spreadsheet generation | Content-generation workflow, not an agent tool-call primitive |

Use this Actor when the caller is a machine that must send conversation state, tool schemas and fallbacks, then decide the next step from `tool_calls` or `structured_output`.

### Agent / tool-call example

```json
{
  "requests": [
    {
      "id": "req-1",
      "model": "openai/gpt-4o-mini",
      "messages": [
        { "role": "user", "content": "Find the weather for Belgrade" }
      ],
      "tools": [
        {
          "type": "function",
          "function": {
            "name": "get_weather",
            "description": "Get current weather",
            "parameters": {
              "type": "object",
              "properties": { "city": { "type": "string" } },
              "required": ["city"],
              "additionalProperties": false
            }
          }
        }
      ],
      "toolChoice": "auto",
      "maxTokens": 256,
      "provider": { "requireParameters": true }
    }
  ]
}
```

Typical Dataset row when the model requests a tool:

```json
{
  "id": "req-1",
  "status": "SUCCESS",
  "model_requested": "openai/gpt-4o-mini",
  "model_used": "openai/gpt-4o-mini",
  "fallback_used": false,
  "assistant_message": { "role": "assistant", "content": null },
  "tool_calls": [
    {
      "id": "call_123",
      "type": "function",
      "name": "get_weather",
      "arguments": { "city": "Belgrade" },
      "arguments_valid": true
    }
  ],
  "finish_reason": "tool_calls"
}
```

Execute `get_weather` in your workflow, then send a follow-up request with a `role=tool` message. This Actor stops after returning the model response.

### Input

Only `requests` (1–25 items). Each item:

| Field | Purpose |
| --- | --- |
| `id` | Stable caller id, returned on the Dataset row |
| `model` or `models[]` | Primary model and/or ordered fallback chain |
| `messages` | Full conversation (1–40 turns). Roles: `system`, `user`, `assistant`, `tool`, `developer` |
| `tools` | Up to 32 OpenAI function tools |
| `toolChoice` | `auto` / `none` / `required`. The Input Schema stores those strings. A function object is accepted by the local runtime only. |
| `responseFormat` | omit, `json_object`, or `json_schema` |
| `maxTokens` | 1-8192, default **2048** when omitted. One upstream call waits at most **90 s**; a slower generation is `TIMEOUT` and is not retried |
| `provider.requireParameters` | Prefer endpoints that support the requested tools/schema |

Cross-field rules the Input Schema cannot express: every item needs `model` or a non-empty `models` list. Blank message text is `INVALID_INPUT` and is not sent upstream; an assistant turn may omit content when it carries `toolCalls`. Duplicate ids fail only the later items.

Default Console prefill asks `openai/gpt-4o-mini` to reply `ping` with `maxTokens=32`. Expect one `SUCCESS` row and a `RUN_SUMMARY` within five minutes.

### Output

One Dataset row per input request, plus `RUN_SUMMARY` and `BILLING_RECEIPT` in the Key-Value Store.

| Field | Meaning |
| --- | --- |
| `status` | `SUCCESS`, `FAILED` or `PARTIAL` |
| `model_requested` / `model_used` / `fallback_used` | Fallback reporting |
| `assistant_message` | Assistant turn; `content` may be null on tool calls |
| `tool_calls` | Normalized function calls; malformed JSON arguments are **not** coerced to `{}` |
| `structured_output` | Parsed JSON when a response format was requested and valid |
| `error.code` | `INVALID_INPUT`, `UNSUPPORTED_MODEL`, `RATE_LIMIT`, `AUTH_FAILED`, `UPSTREAM_4XX`, `UPSTREAM_5XX`, `TIMEOUT`, `DEADLINE_EXCEEDED`, `MALFORMED_UPSTREAM`, `STRUCTURED_OUTPUT_INVALID`, … |
| `fingerprint` | SHA-256 over semantic fields |

`PARTIAL` means useful text or a valid tool call was retained, but another part failed validation or the response was truncated. Inspect `error` before using it. Empty, refused, reasoning-only, and wholly malformed tool responses are uncharged `FAILED`. A batch containing any partial row has a `PARTIAL` run summary.

### Model fallback

OpenRouter performs fallback. This Actor does **not** retry across models itself.

- `model` only: no fallback chain
- `models` only: first id is primary, the rest are fallbacks
- `model` + `models`: primary plus extra fallbacks

`fallback_used` is true when `model_used` differs from the first requested model. Transport retries (429/5xx/connection errors) are a separate mechanism.

OpenRouter rejects an unknown model id before it consults `models[]`. An unknown id in the fallback list is removed and the same request is sent again without it. An unknown primary model stays `UNSUPPORTED_MODEL` with `fallback_used: false` and is not replaced or charged.

### Structured output

- Plain text: omit `responseFormat`
- `json_object`: parse assistant content as a JSON object
- `json_schema`: pass the caller schema upstream and validate the result

Local validation uses JSON Schema Draft 2020-12, including numeric/string bounds, unions and closed objects. Schemas must be self-contained: `$ref`, `$dynamicRef`, `$recursiveRef` and `$id` are rejected before the model call, so schema validation cannot fetch a URL or file. `format` remains a JSON Schema annotation; it is not a semantic validator for dates, email addresses or business facts. Nesting is limited to 32 schema levels. Returned JSON and tool arguments must contain finite numbers and stay within 64 data nesting levels; rejected structured output remains explicit rather than being silently changed.

When tools or a response format are present, the Actor sets `provider.require_parameters=true` unless you override it. Not every model/provider supports tools plus strict schema together; capability errors are returned as `UNSUPPORTED_PARAMETER`, never as an empty success.

### Batch / API usage

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("automa-flow/openrouter-agent-gateway").call(
    run_input={
        "requests": [
            {
                "id": "req-1",
                "model": "openai/gpt-4o-mini",
                "messages": [{"role": "user", "content": "Reply with exactly: ping"}],
                "maxTokens": 32,
            }
        ]
    },
    timeout_secs=300,
    memory_mbytes=256,
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["id"], item["status"], item.get("assistant_message"))
```

Schedule the same batch after a new user message, a tool result, or a new project. A copy-pasteable webhook target is the Apify Dataset items URL from `RUN_SUMMARY.datasetItemsUrl`.

Set `maxTotalChargeUsd` on the run (minimum **$0.013** for gateway start + one successful call). This limit covers gateway events only, not nested OpenRouter charges. Completed rows stay in the Dataset if the budget later runs out; skipped work is `FAILED` with `BUDGET_EXCEEDED`.

### MCP / AI-agent usage

Direct Actor tool: `https://mcp.apify.com?tools=automa-flow/openrouter-agent-gateway`

**Select this Actor when** you need an OpenAI-compatible chat completion with custom tools, structured JSON, or model fallbacks, billed through Apify without provider keys.

**Do not select it for** scraping websites, executing tools, running an autonomous agent loop, bulk spreadsheet prompting, image generation, or streaming tokens.

Ask: “Call openai/gpt-4o-mini with this conversation and these tools; return tool\_calls without executing them.”

`allowsAgenticUsers` is **not** claimed until live Store/API verification after deployment. The local contract is PPE-only, limited permissions, non-Standby.

### Pricing

Two layers, both on the **caller's** Apify account:

1. **Gateway (this Actor, fixed 256 MB):** `apify-actor-start` $0.01 + `agent-call-success` **$0.003** per SUCCESS or PARTIAL request. Failed requests are not charged. Extra transport attempts are not extra gateway events.
2. **Model usage:** nested `apify/openrouter` `openrouter-api-usage` at OpenRouter rates. Each upstream POST can incur that charge, including a 429/5xx/connection-error retry (up to 3 attempts) and a native `models[]` fallback. A timed-out generation is sent once and never retried. Gateway PPE does not cover those nested charges.

Gateway prices include this Actor's platform usage. Model usage is separate. At the supported 256 MB, the start event is charged once. A single successful request costs **$0.013**, 10 requests in one run **$0.040**, and 25 **$0.085**, plus model usage. 1,000 calls cost $3.40 when packed into 40 full batches, or $13 in 1,000 separate runs.

The proxy rounds paid-user model usage up to $0.00001. FREE-plan users pay a 10x model-usage rate and have a 2048-output-token proxy limit. Proxy start events may also apply. Raw `usage.cost` is the upstream model observation, not an invoice. See the [official proxy pricing](https://apify.com/apify/openrouter).

Every request sets provider price ceilings of **$10 per million input tokens**, **$30 per million output tokens**, and **$0 per-request fee**. Providers above those ceilings are excluded, including in a fallback chain. Input is text-only and bounded to 32 KiB per request; completion tokens are always capped. These controls bound workload and provider rates. They are not an atomic dollar cap across nested proxy requests, retries, rounding and account charges. Use a funded account whose model spending policy you control; an autonomous agent must not treat `maxTotalChargeUsd` as the total account bill.

`RUN_SUMMARY.gatewayChargedAmountUsd` reports gateway start plus custom charges. The legacy `chargedAmountUsd` remains custom events only, with `chargedAmountScope` identifying that scope. `nestedModelChargedAmountUsd` stays null because the gateway does not reconcile the proxy invoice. `observedModelUsageCostUsd` sums only returned raw usage and may omit timed-out or lost responses.

### Failure semantics

| Situation | Row status | Run |
| --- | --- | --- |
| Valid completion | `SUCCESS` | `SUCCEEDED` |
| Completion with invalid JSON / malformed tool args | `PARTIAL` | `SUCCEEDED` |
| Item-level 400 / invalid input / duplicate id | `FAILED` | other items continue |
| 429 / 5xx / connection error | retry up to 3 attempts, then `FAILED` | fail the run only if **every attempted** call is auth/timeout/transport/5xx; each POST can still incur nested OpenRouter usage |
| No response within 90 s | `FAILED` `TIMEOUT`, sent once, not retried | other items continue; lower `maxTokens` or pick a faster model |
| Run timeout about to expire | `FAILED` `DEADLINE_EXCEEDED`: the call is cut short or not sent | the run still ends with `RUN_SUMMARY`; send fewer requests or lower `maxTokens` |
| Missing `APIFY_TOKEN`, HTTP 401, or 403 `access_denied` | `AUTH_FAILED` | `SOURCE_FAILED` when every attempted call fails that way |
| Other 403 (content policy, geo, provider refusal) | `FAILED` `UPSTREAM_4XX` | other items continue |

HTTP errors never become empty successful LLM responses.

### Run timeout

Requests run 4 at a time with a **120-second total batch work budget**, including retries and fallback cleanup. Each upstream POST waits at most **90 seconds**. Raising the platform timeout does not extend the work budget. The default platform timeout is 180 seconds at 256 MB.

The Actor also reserves 20 seconds before an earlier platform timeout to persist results and its summary. Requests cut short or not sent receive uncharged `DEADLINE_EXCEEDED` rows. Send smaller batches or lower `maxTokens` for slow models. Platform termination or storage outages can still prevent final summary delivery; the persisted delivery checkpoint prevents automatic double billing after a restart.

### Limitations

- The Actor invokes LLMs and returns requested tool calls. **It does not execute those tools or run an autonomous agent loop.**
- No streaming in this version.
- No browser, proxy configuration, or arbitrary outbound URLs - only the official Apify OpenRouter endpoint.
- Tool + strict-schema support is model/provider dependent.
- Local/non-Actor HTTP to `openrouter.apify.actor` is rejected (`APIFY_ACTOR` only). Call this Actor on Apify instead.
- Nested OpenRouter usage cost is separate from the $0.003 gateway event.

Legal: user-controlled calls to an official Apify LLM proxy. Classification **LOW**. Do not put secrets in `messages`; error logs redact tokens and Authorization headers.

# Changelog

This Actor's version history is a separate document: https://apify.com/automa-flow/openrouter-agent-gateway/changelog.md

# Actor input Schema

## `requests` (type: `array`):

1-25 OpenRouter chat-completion requests. One Dataset row is written per item. Malformed items become uncharged FAILED rows without stopping valid items. Custom tools are forwarded, never executed.

## Actor input object example

```json
{
  "requests": [
    {
      "id": "demo-1",
      "model": "openai/gpt-4o-mini",
      "messages": [
        {
          "role": "user",
          "content": "Reply with exactly: ping"
        }
      ],
      "maxTokens": 32
    }
  ]
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

## `billingReceipt` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "requests": [
        {
            "id": "demo-1",
            "model": "openai/gpt-4o-mini",
            "messages": [
                {
                    "role": "user",
                    "content": "Reply with exactly: ping"
                }
            ],
            "maxTokens": 32
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/openrouter-agent-gateway").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "requests": [{
            "id": "demo-1",
            "model": "openai/gpt-4o-mini",
            "messages": [{
                    "role": "user",
                    "content": "Reply with exactly: ping",
                }],
            "maxTokens": 32,
        }] }

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/openrouter-agent-gateway").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "requests": [
    {
      "id": "demo-1",
      "model": "openai/gpt-4o-mini",
      "messages": [
        {
          "role": "user",
          "content": "Reply with exactly: ping"
        }
      ],
      "maxTokens": 32
    }
  ]
}' |
apify call automa-flow/openrouter-agent-gateway --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/openrouter-agent-gateway"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/sQ6j2KkRPFlPbC2RY/builds/3P1wnIXtms8NKcniW/openapi.json
