# Call a hosted environment with the OpenAI or Anthropic SDK

Point the provider SDK you already use at a hosted Team Agent environment, name the prompt as the model, and know what is accepted, ignored, and refused.

An environment can run **hosted**: AirPrompter runs the promoted version on the organization’s model access and meters it, and there is no store, no model key, and no SDK of ours on your side. You call it with the OpenAI or Anthropic SDK you already use. Change the base URL and the key, name the prompt as the model, and nothing else at the call site changes.

**The release owns the system prompt and the settings**

A compatible call cannot add instructions, add tools, pick a model, change temperature, or reach a prompt the key’s environment has not promoted. It meters on the organization’s allowance exactly as a normal hosted run, leaves the same content-free run record, and is sampled for review at the same rate. If your call site needs any of those, the call is not governed and the endpoint refuses it by name.

## Before you start

You need:

- an environment whose sync mode is hosted, with a prompt promoted to it;
- a **run key** for that environment from the agent’s **Settings → Keys** (a run key is a separate kind from an Agent key: it may execute and spend, and is never co-granted by default), stored as `AIRPROMPTER_RUN_KEY`;
- the environment’s **run URL**, shown on the environment’s Settings; and
- a prompt that declares exactly one end-user variable, which is where the user turn goes.

## Point the SDK at the environment

| SDK                                | Base URL                                          | Path the SDK appends              |
| ---------------------------------- | ------------------------------------------------- | --------------------------------- |
| `openai` (any language)            | `https://<run-url>/v1/agents/<agentId>/openai`    | `/chat/completions`, `/responses` |
| `@anthropic-ai/sdk` or `anthropic` | `https://<run-url>/v1/agents/<agentId>/anthropic` | `/v1/messages`                    |

The environment is the key’s own: a run key is bound to one environment, so the path carries no environment segment. The key goes where the SDK puts it, `Authorization: Bearer` for OpenAI or `x-api-key` for Anthropic; both reach the same check.

```ts
import OpenAI from "openai";

const openai = new OpenAI({
  baseURL: `https://${process.env.AIRPROMPTER_RUN_URL}/v1/agents/${process.env.AIRPROMPTER_AGENT_ID}/openai`,
  apiKey: process.env.AIRPROMPTER_RUN_KEY,
});

const completion = await openai.chat.completions.create({
  model: "slot:support.triage",                        // the prompt's key in code, never a model name
  messages: [{ role: "user", content: ticketText }],   // the one user turn: the prompt's end-user variable
  airprompter: { variables: { team: "Billing" } },     // the prompt's other declared variables
  user: "customer-42",                                 // sticky arm assignment during a rollout; hashed on arrival, never stored
} as any);

completion.choices[0].message.content;
(completion as any).airprompter.runRef;                // also in the X-AirPrompter-RunRef response header
```

```python
import os
import anthropic

client = anthropic.Anthropic(
    base_url=f"https://{os.environ['AIRPROMPTER_RUN_URL']}/v1/agents/{os.environ['AIRPROMPTER_AGENT_ID']}/anthropic",
    api_key=os.environ["AIRPROMPTER_RUN_KEY"],
)

message = client.messages.create(
    model="slot:support.triage",
    max_tokens=400,                                     # required on Messages
    messages=[{"role": "user", "content": ticket_text}],
    metadata={"user_id": "customer-42"},                # sticky arm assignment
    extra_body={"airprompter": {"variables": {"team": "Billing"}}},
)
```

The `airprompter` extension is an extra body field. The OpenAI SDKs accept unknown fields at the top level in TypeScript with a cast; in Python pass it through `extra_body=`.

**Expected result:** the answer is the provider’s own shape with `model` echoed as `slot:support.triage`, an `airprompter` field carrying `runId` and `runRef` beside the standard fields, and the same `runRef` in the `X-AirPrompter-RunRef` header.

## What the mapping does

| Field in your call                                                                                           | Becomes                                              | Rule                                                                                                                                                                                     |
| ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`                                                                                                      | The slot                                             | Must be `slot:<tag>`; a real model name is refused `model_not_a_slot`                                                                                                                    |
| A `system` or `developer` message, `instructions`, or `system`                                               | Nothing                                              | Refused `system_message_not_allowed`: the sealed template is the system prompt                                                                                                           |
| The one user turn                                                                                            | The prompt’s single end-user variable                | Exactly one turn of text; a second turn, an assistant turn, an image, audio, or a tool result is `unsupported_messages`                                                                  |
| `airprompter.variables`                                                                                      | The other declared variables                         | Unknown names are `render_unknown_variable`; missing required ones `render_missing_variable`; an optional variable you leave out renders the default its prompt settings declare, if any |
| `airprompter.subjectHash`, else `user` (OpenAI) or `metadata.user_id` (Anthropic)                            | The rollout subject                                  | Hashed on the way in and never stored                                                                                                                                                    |
| `max_completion_tokens` / `max_tokens` / `max_output_tokens`                                                 | The output cap                                       | Above the release’s ceiling is refused; `max_tokens` is required on Messages                                                                                                             |
| `stream`                                                                                                     | Server-sent events in the provider’s own event shape | The SDK’s `stream()` helpers assemble them                                                                                                                                               |
| `airprompter.idempotencyKey` (or the `Idempotency-Key` header), `airprompter.stepId`, `airprompter.metadata` | The same as a direct hosted run                      | The same replay window, workflow step, and echoed metadata                                                                                                                               |

**Accepted and ignored**, because the release owns them: `temperature`, `top_p`, `stop` / `stop_sequences`, `top_k`, `presence_penalty`, `frequency_penalty`, `seed`, `logit_bias`, `store`, `stream_options`, `service_tier`, `reasoning_effort` / `reasoning`, `verbosity`, `parallel_tool_calls`, `prompt_cache_key`, `safety_identifier`, `logprobs`, `top_logprobs`, `truncation`, `include`, `text`, `thinking`. The run uses the slot’s own inference settings, sealed with the version.

**Refused by name** (`unsupported_parameter`, the name in `detail`): `tools`, `tool_choice`, `functions`, `function_call`, `response_format`, `audio`, `modalities`, `prediction`, `web_search_options`, `n` above 1, `previous_response_id`, `conversation`, `background`, `prompt`, `mcp_servers`, `container`. Tool calling beyond what the release renders is out of scope; so is bringing your own model key.

## Read the answer

| Run outcome | Chat Completions                                                       | Responses                                                            | Messages                                           |
| ----------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------- |
| Finished    | `finish_reason: stop`                                                  | `status: completed`                                                  | `stop_reason: end_turn`                            |
| Hit the cap | `finish_reason: length`                                                | `status: incomplete`                                                 | `stop_reason: max_tokens`                          |
| Usage       | `prompt_tokens` includes cached; `prompt_tokens_details.cached_tokens` | `input_tokens` includes cached; `input_tokens_details.cached_tokens` | `input_tokens` uncached; `cache_read_input_tokens` |

Streams follow each provider’s event sequence; usage is always sent on the final event (Chat needs no `stream_options.include_usage`). A refusal after the stream began is the provider’s error event on the open stream.

Every refusal is the provider’s error shape with the status a direct hosted run would answer, plus the AirPrompter code beside it:

```json
{ "error": { "message": "…", "type": "invalid_request_error", "param": null, "code": "model_not_a_slot" },
  "airprompter": { "code": "model_not_a_slot", "detail": "gpt-5" } }
```

| Code                         | Status | Meaning                                                                                                    |
| ---------------------------- | ------ | ---------------------------------------------------------------------------------------------------------- |
| `model_not_a_slot`           | 400    | `model` is not `slot:<tag>`                                                                                |
| `system_message_not_allowed` | 400    | A system or developer message, `instructions`, or `system`                                                 |
| `unsupported_messages`       | 400    | More than one turn, an assistant turn, a non-text part, a tool result                                      |
| `unsupported_parameter`      | 400    | A refused parameter, named in `detail`, or a malformed `airprompter` extension                             |
| `no_end_user_variable`       | 400    | The prompt declares no single end-user variable for the user turn                                          |
| `max_tokens_required`        | 400    | Messages only                                                                                              |
| `slot_not_found`             | 404    | No such prompt promoted to this environment                                                                |
| `allowance_exhausted`        | 402    | The organization is out of AI credits; with auto refill on, `Retry-After` says when to try again           |
| `rate_limited`               | 429    | Retry after the `Retry-After` header                                                                       |
| `model_unavailable`          | 503    | The pinned model cannot be reached right now                                                               |
| `meter_unavailable`          | 503    | Billing facts the meter needs could not be read; nothing was charged. Retry after the `Retry-After` header |

## Use feedback and the hosted client

The `runRef` the endpoint returns is the same token a direct hosted run mints. Keep it beside your own record and file feedback later from any process holding the run key, with the thin hosted client:

```ts
import { ManagedAgent } from "@airprompter/agent-sdk";

const hosted = await ManagedAgent.start({ agentId, target: "prod", apiKey: process.env.AIRPROMPTER_RUN_KEY!, baseUrl: runUrl });
await hosted.feedback(runRef, { accepted: true, rating: 4 });
```

`ManagedAgent` also runs prompts directly (`run`, `stream`, `workflow`) when you want typed refusals and the catalogue of slots, variables, and steps; in Python it is `ManagedAgent.start(agent_id=, target=, api_key=, base_url=)`. Sources your app registers (`ManagedAgent.start({ variables })`; Python `ManagedAgent.start(variables=...)`, plain callables only) fill the required declared variables, and any the prompt’s settings mark as filled by your app, before the run is posted — the hosted route has no way into your systems, so a source whose trust is stricter than the prompt’s declaration is refused rather than sent unfenced. The compatible endpoints exist for the call sites you do not want to change.

## Apply it to day-to-day work

Move an existing OpenAI-based feature under governance without rewriting its call site:

1. Keep the same client; swap the base URL and the key from configuration.
2. Replace the model name with `slot:<tag>`.
3. Delete the system prompt from the request, because the release now owns it, and move any operator-trust values into `airprompter.variables`.
4. Store the `runRef` from the answer beside your own record.

What used to be a prompt change in your code is now a promotion on the board.

## Troubleshooting

### `system_message_not_allowed`

Remove the `system` or `developer` message (or `instructions` / `system`) from the call. The release’s template is the system prompt; put operator-trust values in `airprompter.variables`.

### `no_end_user_variable`

The prompt declares zero or several end-user variables. Either declare exactly one, or pass every value through `airprompter.variables` and send an empty user turn.

### `unsupported_parameter` naming `tools`

Tool calling through the compatible endpoint is out of scope. Run tools in your own code and call the endpoint for the prompt only, or run the prompt from your own runtime with the [TypeScript](https://airprompter.com/docs/integrations/typescript) or [Python](https://airprompter.com/docs/integrations/python) SDK.

### The temperature you sent had no effect

That is by design: the release’s sealed settings are used and the caller’s are named on the run’s log line as ignored.

## Completion checklist

- The run key reaches the process only through the environment, and it is a run key, not an Agent key.
- `model` is `slot:<tag>` and no system message is in the call.
- The answer carries `airprompter.runRef` and you store it beside your record.
- Your error handling reads `airprompter.code` beside the provider’s error shape.
- Rollout stickiness passes `user` or `metadata.user_id`, never a raw identifier you need to keep private.

## Next steps

If a feature outgrows one user turn, or needs tools, move it to your own runtime with the resident [TypeScript](https://airprompter.com/docs/integrations/typescript) or [Python](https://airprompter.com/docs/integrations/python) integration; the prompt’s key in code is the same.

---

Canonical: https://airprompter.com/docs/integrations/hosted
