OpenAPI¶
Available · importer · pip install "agent-adapter[openapi]" (httpx, pyyaml)
import_openapi turns every operation of an OpenAPI 3.x document into a tool. The tools
call the API over httpx and run through the executor like any other tool.
Functions¶
Name (agent_adapter.integrations.openapi) |
What |
|---|---|
import_openapi(registry, spec, *, base_url=None, auth=None, headers=None, operations=None, prefix="", timeout=30.0, client_factory=None, **policy) |
the new ToolSpecs, added all or nothing |
BearerAuth(token) |
Authorization: Bearer …; token is a string or a sync/async callable asked per call |
HeaderAuth(name, value) / QueryAuth(name, value) |
a fixed header / query parameter (prefer the header: see Credentials) |
INTEGRATION |
the integration object (import only: export_tool raises IntegrationError) |
from agent_adapter.integrations.openapi import BearerAuth, import_openapi
specs = import_openapi(
registry,
DOCUMENT, # a mapping, JSON/YAML text, or a file path
auth=BearerAuth("s3cr3t"), # or BearerAuth(get_token), refreshed per call
retries=2, # policy options apply to every imported tool; 429/5xx are retried
client_factory=client_factory, # omit in production: httpx.AsyncClient
)
rate = await Executor(registry).run("getRate", {"item": "laptop", "X_Region": "EU"})
Options¶
| Option | Default | Meaning |
|---|---|---|
spec |
— | a mapping, JSON or YAML text (has a newline or starts with {), or a file path; URLs are refused |
base_url |
servers[0].url with variable defaults |
must be absolute |
headers |
none | sent with every request |
operations |
all | operationIds (or {method}_{path} keys) to import; unknown ones are a ConfigError |
prefix |
"" |
added to every tool name |
client_factory |
httpx.AsyncClient |
called as client_factory(base_url=, headers=, timeout=), once per event loop |
timeout |
30.0 |
seconds for the whole request, connect to last body byte (also the client's per-phase timeout) |
**policy |
kind="http", tags {"openapi", *operation tags}, effects, idempotent |
any tool option (retries=, confirm=, cache_ttl= ...); timeout= is the HTTP timeout above, not the tool option |
One operation, one tool¶
flowchart LR
OP["operation"] --> N["name: operationId<br/>else method_path"]
OP --> D["description: description<br/>else summary"]
P["path · query · header · cookie<br/>parameters"] --> S[("flat input schema")]
B["JSON request body"] --> S
S --> T["spec_from_schema"]
N --> T
D --> T
| Rule | Example |
|---|---|
Names are sanitised to [A-Za-z0-9_] |
X-Trace → X_Trace, page[size] → page_size, GET /pets/{id} → get_pets_id |
| Two parameters with one name | the later one gets its location: id (path), query_id |
| A name already taken | numbered from _2: dup, dup_2, dup_3 (tool names stay within 64 characters) |
| One operationId on several operations | all imported with numbered names; one WARNING per import lists the duplicated ids |
| Object body | its properties join the schema; a clash gets body_ (name → body_name) |
Array, scalar, map or allOf/oneOf body |
one body field (body_body on a clash) |
readOnly body properties |
left out |
$ref |
local refs inlined; a recursive ref becomes {} (any value); remote refs are a ConfigError. Within one tool, repeated targets are one shared object: copy.deepcopy(spec.input_schema) before editing it in place |
OpenAPI 3.0 nullable: true |
type: [X, "null"] |
Accept, Content-Type, Authorization header parameters |
ignored (OpenAPI rule; use auth=) |
| Non-JSON request body (form, multipart) | operation skipped (see Untrusted documents) |
effects |
read_only for GET/HEAD, destructive for DELETE, else additive |
idempotent |
GET, HEAD, PUT, DELETE |
| Result schema | the lowest 2xx JSON response |
Unset optional arguments are not sent, so the API applies its own defaults. Query arrays
repeat the key unless the parameter says explode: false (comma-joined); objects in
query, header and cookie values are compact JSON. Path values are one percent-encoded
segment (/ and . included); "", ., .. and null are ToolArgsInvalid, as are
header and cookie values that are not printable ASCII (no CR/LF).
Responses and retries¶
| Outcome | Tool result |
|---|---|
| 2xx | the JSON body, else the text; None for 204 or an empty body |
| 3xx, 4xx except 429 | ToolError("HTTP 404 from GET /pets/{petId}: <first 500 chars>"), details={"status": …}; not retried |
| 429 | httpx.HTTPStatusError (message, request and response without the query string); retried up to retries, then ToolFailed |
| 5xx, idempotent operation | as 429 |
| 5xx, non-idempotent operation (POST, PATCH, ...) | ToolError(... "not retried because POST is not idempotent"): the server may have acted |
| connection refused / connect or pool timeout | the httpx error; retried (nothing was sent) |
| other transport errors (read timeout, dropped connection) | retried for idempotent operations; ToolError for the others |
| body over a fixed 10 MiB response cap (raw or decoded) | ToolError; the rest is not read |
Content-Encoding other than gzip / deflate / identity, or corrupt, truncated or trailing compressed data |
ToolError (we send Accept-Encoding: gzip, deflate, identity and inflate with bounded output; concatenated gzip members are all decoded; NUL padding after the last member is ignored) |
no complete response within timeout (whole request) |
httpx.ReadTimeout, handled as a transport error |
Idempotent means GET, HEAD, PUT and DELETE. Pass idempotent=True to opt every imported
operation (POST included) into 5xx retries when the API dedupes requests itself. Before a
retry of a 429 or retryable 503, a Retry-After (seconds or HTTP date) is waited for, at
most 30 s, and only when the executor will make another attempt (it says so through
exec.pipeline.current_attempt(); a direct spec.invoke never waits). The executor's backoff
follows, so the worst-case wait before one retry is min(Retry-After, 30 s) plus
Backoff.max_s (30 s by default). Redirects are never followed, whatever the
client_factory client is configured to do, so credentials cannot follow a 3xx elsewhere.
Untrusted documents¶
A document decides where requests go, so treat third-party specs as code:
- Always pass
base_url=soserversin the document cannot redirect calls. - The server URL may not carry credentials (
user:pw@: put them inauth=) or an ambiguous numeric host (0x7f.1,0177.0.0.1,2130706433,127.1: writea.b.c.d); server variable values may not contain@,/,\,?or#. Schemes match in any case. - Path keys must be plain absolute paths (no scheme,
//,?,#,\,.or..segment) and their{placeholders}must match the path parameters (ConfigError). - Each call checks scheme, host and port against
base_urlbefore credentials are added. - Only local
$refs are followed. A repeated$refis built once and shared, so the inlining work stays linear in the document. Bounds per operation skip that operation (or drop its response schema); document-wide budgets refuse the import:
| Bound | Limit | Over it |
|---|---|---|
| one resolved schema, parameter or body (expanded) | 10 000 values, 128 levels | operation skipped; a response schema is dropped instead |
| the document, YAML aliases expanded | 2 000 000 values | ConfigError |
| values built while inlining (shared values count once) | 64 per document value, 50 000 to 1 000 000; half for response schemas | ConfigError; response schemas are dropped instead |
| values all imported tools hold (each tool copies its schemas) | 64 per document value, 50 000 to 4 000 000 | ConfigError; response schemas are dropped instead |
| top-level input fields of all tools (each is a pydantic field, built on first use) | 8 per document value, 5 000 to 100 000; checked before an operation holds anything | operations that no longer fit are skipped |
An import logs at most three WARNINGs, each naming the first five entries: duplicated
operationIds, skipped operations (with the reason: not JSON, too large, too deep, over
the fields budget) and dropped response schemas.
An operation named in operations= that a bound skips is a ConfigError (after the
summary is logged).
- The origin check compares host names, not addresses: DNS rebinding (a name that later
resolves elsewhere) is out of scope; pin DNS or use an egress proxy for that.
- Malformed shapes are a ConfigError naming the operation.
Credentials¶
Credentials never appear in our logs or error messages; the auth objects' repr hides
them. Prefer HeaderAuth or BearerAuth: QueryAuth values are masked in this process's
httpx log lines, but servers, proxies and their access logs still record query strings.