# External Models API

> Run external chat models with their native input and output. The maximum charge is reserved at the start and the exact charge is taken once.

The External Models API runs a chat model of an external provider with that model's **native** input and returns its **native** output unchanged. Every model has a versioned contract with its input and output JSON Schemas and its per-token price. The maximum charge is reserved on your FlyMyAI balance when you start an operation, and the exact charge for the reported usage is taken once when it finishes.

Route keys name the provider and the model, for example `openrouter/meta-llama/llama-3.1-8b-instruct`.

## Authentication and base URL

- Base URL: `https://backend.flymy.ai/api/v1/model-proxy/`
- Header: `X-API-KEY: <your FlyMyAI API key>` (see [Authentication](https://docs.flymy.ai/authentication.md)). Web sessions can use their bearer token instead.
- A request without credentials gets `403`.

## 1. Find a model

`GET /catalog/?prefix=<route prefix>&limit=<1..100>&cursor=<next_cursor>` lists the models you can run:

```json
{"release": "d2a3f6eb-…", "items": [{
  "profile": "openrouter.chat.v1", "operation": "chat.completions",
  "route_key": "openrouter/meta-llama/llama-3.1-8b-instruct",
  "model_version": "meta-llama/llama-3.1-8b-instruct",
  "contract_revision": "91e3f184ebc3003f6fdec0544c1de1b91add6a118d1fd44df00e69473355784d"}],
 "next_cursor": null}
```

A cursor is valid for one hour, for the same user and prefix.

## 2. Read the contract

`GET /contracts/<contract_revision>/` returns the exact contract:

- the native input and output JSON Schemas (`artifact.schemas`);
- the price (`artifact.price`);
- the limits: input and output bytes, and a deadline;
- `execution_constraints`.

The revision is a SHA-256 of the contract, so a newer catalog never changes an operation you already started.

## 3. Start an operation

`POST /operations/` with an `Idempotency-Key` header (your unique key for this exact request) and the native input:

```http
POST /api/v1/model-proxy/operations/
X-API-KEY: <key>
Idempotency-Key: order-42-summary
Content-Type: application/json

{"contract_revision": "91e3f184…",
 "inputs": {"model": "meta-llama/llama-3.1-8b-instruct",
            "messages": [{"role": "user", "content": "Reply with exactly OK."}],
            "max_tokens": 8, "temperature": 0}}
```

The answer is `202 Accepted` with `"created": true`:

```json
{"operation": {"operation_id": "6fdc75e7-…", "prediction_id": "6fdc75e7-…",
  "status": "queued", "stage": "submit",
  "contract_revision": "91e3f184…", "cancel_requested": false},
 "created": true}
```

Repeating the same key with the same body returns the same operation (`200`, `"created": false`) and never starts a second paid call. The same key with a different body gets `409 idempotency_key_reused`.

## 4. Wait for the result

Poll `GET /operations/<operation_id>/` until `status` is final. When it is `succeeded`, `output` holds the provider's native result:

```json
{"operation": {"operation_id": "6fdc75e7-…", "status": "succeeded", "stage": "submit",
  "output": {"id": "gen-…", "object": "chat.completion",
    "choices": [{"index": 0, "message": {"role": "assistant", "content": "OK."},
                 "finish_reason": "stop"}],
    "usage": {"prompt_tokens": 32, "completion_tokens": 3, "total_tokens": 35}}}}
```

| Status | Meaning |
| --- | --- |
| `queued` | Admitted and waiting for a provider slot |
| `running` | Sent to the provider |
| `succeeded` | Final; `output` is set and the exact charge is taken |
| `failed` | Final; no charge (for example, the request never reached the provider) |
| `cancelled` | Final; cancelled before it was sent, no charge |
| `rejected` | Final; refused before it was sent, no charge |
| `unknown` | The provider may have done the work but did not answer clearly. The maximum charge stays reserved until FlyMyAI reconciles it with the provider; the request is never sent twice |

## 5. Cancel

`POST /operations/<operation_id>/cancel/` with the body `{}`.

- **Before the request was sent:** the operation ends `cancelled` and the reservation is released.
- **After it was sent:** the provider offers no proven way to cancel or refund, so `cancel_requested` becomes `true`, and the result is still delivered and charged.

## Prices and charges

- **At the start** the maximum charge is reserved on your balance: the contract's per-million token rates (`kind: "tokens"`) times its `max_usage`, the most one call can use.
- **On success** the exact charge is taken once: the provider's reported usage at the contract's per-million rates. The rest of the reservation is released.
- **Failed, cancelled and rejected operations** release the reservation without a charge.
- **An `unknown` outcome** keeps the reservation until reconciliation.

## Errors

| HTTP | `code` | When |
| --- | --- | --- |
| 400 | `proxy_input` | The input does not match the contract's native schema (nothing reserved, nothing sent) |
| 400 | `invalid_idempotency_key`, `proxy_contract`, `proxy_json`, `proxy_profile_constraint`, `proxy_prompt_bound` | Missing or invalid key, missing exact revision, text that cannot be stored, a profile rule (for example `stream: true`) or a prompt above the contract's priced bound |
| 402 | `proxy_balance` | The balance cannot cover the maximum charge |
| 403 | - / `proxy_permission`, `proxy_route_stale` | No credentials; no access to the model; an inactive account or a contract past its validity |
| 404 | `proxy_contract_missing`, `proxy_operation_missing` | Unknown or invisible contract; an operation of another payer |
| 409 | `idempotency_key_reused` | Same key, different request |
| 409 | `proxy_route_revoked`, `proxy_route_disabled` | This contract revision is no longer enabled: read the catalog again and use the current revision. Nothing is reserved |
| 413 | `proxy_bytes`, `proxy_depth` | Request body above 264 KiB or the contract's input limit; JSON nested too deep |
| 429 | `proxy_pending_limit` | Too many unresolved operations for this payer or provider account; retry later |
| 503 | `proxy_receipt_guard` and other operational codes | Temporary refusal; nothing reserved; retry later |

Errors have the shape `{"code": "...", "detail": "..."}`.

## What you can run

- **Chat** (`openrouter.chat.v1`): the native chat completions input (messages, tools, sampling fields and more) for models with an exact per-token price. No streaming. Each call goes to the model's cheapest available OpenRouter provider that supports every parameter you send, so a provider never silently ignores one, such as `max_tokens`. You cannot choose the provider.
- The catalog lists only models whose contract is complete and verified. Streaming, embeddings, image, video and audio models, realtime models and file uploads are not available through this API.

## Limits

- Request body: 264 KiB. Per contract: `limits.max_input_bytes`, `limits.max_output_bytes` and `limits.deadline_seconds`. An operation not finished by its deadline is not retried with the provider.
- `stream` must be absent or `false`.
- Provider capacity is shared fairly between payers: queued operations of different payers take turns.
