AirPrompter documentation
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.
Available
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.
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.
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
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:
{ "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:
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:
- Keep the same client; swap the base URL and the key from configuration.
- Replace the model name with
slot:<tag>. - Delete the system prompt from the request, because the release now owns it, and move any operator-trust values into
airprompter.variables. - Store the
runReffrom 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 or 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.
modelisslot:<tag>and no system message is in the call.- The answer carries
airprompter.runRefand you store it beside your record. - Your error handling reads
airprompter.codebeside the provider’s error shape. - Rollout stickiness passes
userormetadata.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 or Python integration; the prompt’s key in code is the same.