Skip to content

OpenAPI Tools

OpenAPI lets you turn any REST API into agent tools automatically. Point the agent at an OpenAPI spec (JSON file or URL) and every operation becomes an AIFunction the agent can call — no hand-written code required.

OpenAPI Spec          Generated Tools
┌─────────────┐       ┌──────────────────────────┐
│ GET /users  │  ───► │ stripe_listCustomers()   │
│ POST /users │  ───► │ stripe_createCustomer()  │
│ DELETE /... │  ───► │ stripe_deleteCustomer()  │
└─────────────┘       └──────────────────────────┘

Quick Start

Via AgentBuilder

csharp
var agent = await new AgentBuilder()
    .WithOpenApi(
        name: "stripe",
        specPath: "./stripe.json",
        configure: cfg =>
        {
            cfg.AuthCallback = async (req, ct) =>
                req.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("STRIPE_KEY"));
        })
    .BuildAsync();

Add [OpenApi] to a method inside a toolkit class that returns OpenApiConfig:

csharp
public class StripeToolkit(ISecretResolver secrets)
{
    [OpenApi(Prefix = "stripe")]
    public OpenApiConfig Stripe() => new()
    {
        SpecPath = "stripe.json",
        AuthCallback = async (req, ct) =>
        {
            var key = await secrets.RequireAsync("stripe:ApiKey", "Stripe", ct: ct);
            req.Headers.Authorization = new("Bearer", key);
        }
    };
}

var agent = await new AgentBuilder()
    .WithToolkit<StripeToolkit>()
    .BuildAsync();

ISecretResolver is injected automatically — the source generator detects the constructor and the builder passes its configured resolver at startup. No DI container registration needed.

The toolkit approach also gives you collapsing, permissions, and conditional visibility — the same as any other capability.


The [OpenApi] Attribute

csharp
[OpenApi(Prefix = "stripe")]
public OpenApiConfig Stripe() => new() { ... };
PropertyTypeDescription
Prefixstring?Prefix for generated function names. Functions become {Prefix}_{OperationId}. Defaults to the method name.

Rules:

  • Method must be parameterless (HPDAG0403 is emitted if it has parameters)
  • Must return OpenApiConfig
  • Add [RequiresPermission] to require user approval for all generated functions

OpenApiConfig Reference

OpenApiConfig extends the base config with agent-specific options:

Spec Source (required — pick one)

PropertyTypeDescription
SpecPathstring?Path to a local JSON OpenAPI spec file.
SpecUriUri?URL to fetch the spec from at build time.

HTTP & Auth

PropertyTypeDescription
AuthCallbackFunc<HttpRequestMessage, CancellationToken, Task>?Called before every request. Use to add auth headers.
HttpClientHttpClient?Provide your own HTTP client. You own the lifecycle.
ServerUrlOverrideUri?Override the base URL from the spec.
UserAgentstring?User-Agent header for requests.
TimeoutTimeSpanRequest timeout. Default: 30 seconds. Only applies to internally-created clients.

Operation Filtering

PropertyTypeDescription
OperationSelectionPredicateFunc<OperationSelectionContext, bool>?Include only operations matching this predicate.
OperationsToExcludeIList<string>?Exclude specific operation IDs. Mutually exclusive with the predicate.

Request Body Handling

PropertyTypeDefaultDescription
EnableDynamicPayloadbooltrueFlatten nested body properties into separate parameters. Set to false to pass the body as a single JSON string.
EnablePayloadNamespacingboolfalsePrefix parameter names with parent property names to avoid collisions in deeply nested objects.

Error Handling

PropertyTypeDescription
ErrorDetectorFunc<HttpResponseMessage, string?, OpenApiErrorResponse?>?Detect API-specific errors that return HTTP 200 (e.g. Slack's { "ok": false } pattern).
IgnoreNonCompliantErrorsboolContinue with warnings instead of throwing on non-compliant spec fields.

Agent-Specific (OpenApiConfig only)

PropertyTypeDefaultDescription
RequiresPermissionboolfalseRequire user approval for all generated functions.
CollapseWithinToolkitboolfalseGroup all generated functions behind their own nested container inside the parent toolkit.
SchemaTransformOptionsAIJsonSchemaTransformOptions?nullPost-process generated parameter schemas (e.g. DisallowAdditionalProperties).
ResponseOptimizationResponseOptimizationConfig?nullTrim API responses before they reach the LLM. See below.

Response Optimization

API responses can be large and token-heavy. ResponseOptimizationConfig trims the response before the LLM sees it:

csharp
cfg.ResponseOptimization = new ResponseOptimizationConfig
{
    DataField = "data",                            // Unwrap envelope: { "data": [...] } → [...]
    FieldsToInclude = ["id", "name", "email"],     // Keep only these fields
    MaxLength = 5000                               // Hard cap on response size (chars)
};
PropertyTypeDescription
DataFieldstring?Extract data from a nested envelope field before filtering. Supports dot notation (e.g. "result.data").
FieldsToIncludeIList<string>?Whitelist — only these fields reach the LLM. Mutually exclusive with FieldsToExclude.
FieldsToExcludeIList<string>?Blacklist — strip these fields from the response. Mutually exclusive with FieldsToInclude.
MaxLengthintMax character length of the serialized response. 0 = no limit.

Function Naming

Generated function names follow this pattern: {Prefix}_{OperationId}.

If an operation has no operationId, the name is derived from the HTTP method and path:

  • GET /users/{id}stripe_GetUsersId

Keep operationId values in your spec clean — only A-Z, a-z, 0-9, and _ are kept; all other characters are stripped.


Common Patterns

From a URL

csharp
.WithOpenApi(
    name: "github",
    specUri: new Uri("https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json"),
    configure: cfg =>
    {
        cfg.AuthCallback = async (req, ct) =>
            req.Headers.Authorization = new("Bearer", Environment.GetEnvironmentVariable("GITHUB_TOKEN"));
        cfg.OperationsToExclude = ["repos/delete"]; // don't expose destructive operations
    })

Filter to a subset of operations

csharp
cfg.OperationSelectionPredicate = ctx =>
    ctx.Path.StartsWith("/customers") && ctx.Method == "GET";

Collapse inside a toolkit

ISecretResolver is injected automatically here too:

csharp
[Collapse("Stripe billing tools")]
public class BillingToolkit(ISecretResolver secrets)
{
    [OpenApi(Prefix = "stripe")]
    public OpenApiConfig Stripe() => new()
    {
        SpecPath = "stripe.json",
        CollapseWithinToolkit = true,  // stripe_* grouped behind own sub-container
        AuthCallback = async (req, ct) =>
        {
            var key = await secrets.RequireAsync("stripe:ApiKey", "Stripe", ct: ct);
            req.Headers.Authorization = new("Bearer", key);
        }
    };

    [AIFunction]
    public string FormatInvoice(string invoiceId) { /* ... */ }
}
Before expanding BillingToolkit:
┌──────────────────────┐
│ BillingToolkit       │
│ (Stripe billing...)  │
└──────────────────────┘

After expanding BillingToolkit:
┌──────────────────────┐
│ OpenApi_stripe       │  ← sub-container (CollapseWithinToolkit)
│ FormatInvoice        │  ← native AIFunction, always visible
└──────────────────────┘

After expanding OpenApi_stripe:
┌──────────────────────┐
│ stripe_listCustomers │
│ stripe_createCharge  │
│ stripe_getInvoice    │
│ ...                  │
└──────────────────────┘

With permissions

csharp
public class PaymentsToolkit
{
    [OpenApi(Prefix = "stripe")]
    [RequiresPermission]
    public OpenApiConfig Stripe() => new()
    {
        SpecPath = "stripe.json",
        AuthCallback = ...
    };
}

Custom error detection (Slack-style)

csharp
cfg.ErrorDetector = (response, body) =>
{
    if (body != null
        && JsonDocument.Parse(body).RootElement.TryGetProperty("ok", out var ok)
        && !ok.GetBoolean())
        return new OpenApiErrorResponse { StatusCode = 200, Body = body };
    return null;
};

Error Handling

The runtime automatically handles HTTP errors:

StatusBehavior
429, 401, 408, 5xxThrown as OpenApiRequestException → caught by FunctionRetryMiddleware, retried with backoff
400, 404, 422, other 4xxReturned to the LLM as an error description so it can self-correct its request

Next Steps

Released under the MIT License.