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
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();Via Toolkit Class (Recommended)
Add [OpenApi] to a method inside a toolkit class that returns OpenApiConfig:
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
[OpenApi(Prefix = "stripe")]
public OpenApiConfig Stripe() => new() { ... };| Property | Type | Description |
|---|---|---|
Prefix | string? | 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)
| Property | Type | Description |
|---|---|---|
SpecPath | string? | Path to a local JSON OpenAPI spec file. |
SpecUri | Uri? | URL to fetch the spec from at build time. |
HTTP & Auth
| Property | Type | Description |
|---|---|---|
AuthCallback | Func<HttpRequestMessage, CancellationToken, Task>? | Called before every request. Use to add auth headers. |
HttpClient | HttpClient? | Provide your own HTTP client. You own the lifecycle. |
ServerUrlOverride | Uri? | Override the base URL from the spec. |
UserAgent | string? | User-Agent header for requests. |
Timeout | TimeSpan | Request timeout. Default: 30 seconds. Only applies to internally-created clients. |
Operation Filtering
| Property | Type | Description |
|---|---|---|
OperationSelectionPredicate | Func<OperationSelectionContext, bool>? | Include only operations matching this predicate. |
OperationsToExclude | IList<string>? | Exclude specific operation IDs. Mutually exclusive with the predicate. |
Request Body Handling
| Property | Type | Default | Description |
|---|---|---|---|
EnableDynamicPayload | bool | true | Flatten nested body properties into separate parameters. Set to false to pass the body as a single JSON string. |
EnablePayloadNamespacing | bool | false | Prefix parameter names with parent property names to avoid collisions in deeply nested objects. |
Error Handling
| Property | Type | Description |
|---|---|---|
ErrorDetector | Func<HttpResponseMessage, string?, OpenApiErrorResponse?>? | Detect API-specific errors that return HTTP 200 (e.g. Slack's { "ok": false } pattern). |
IgnoreNonCompliantErrors | bool | Continue with warnings instead of throwing on non-compliant spec fields. |
Agent-Specific (OpenApiConfig only)
| Property | Type | Default | Description |
|---|---|---|---|
RequiresPermission | bool | false | Require user approval for all generated functions. |
CollapseWithinToolkit | bool | false | Group all generated functions behind their own nested container inside the parent toolkit. |
SchemaTransformOptions | AIJsonSchemaTransformOptions? | null | Post-process generated parameter schemas (e.g. DisallowAdditionalProperties). |
ResponseOptimization | ResponseOptimizationConfig? | null | Trim 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:
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)
};| Property | Type | Description |
|---|---|---|
DataField | string? | Extract data from a nested envelope field before filtering. Supports dot notation (e.g. "result.data"). |
FieldsToInclude | IList<string>? | Whitelist — only these fields reach the LLM. Mutually exclusive with FieldsToExclude. |
FieldsToExclude | IList<string>? | Blacklist — strip these fields from the response. Mutually exclusive with FieldsToInclude. |
MaxLength | int | Max 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
.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
cfg.OperationSelectionPredicate = ctx =>
ctx.Path.StartsWith("/customers") && ctx.Method == "GET";Collapse inside a toolkit
ISecretResolver is injected automatically here too:
[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
public class PaymentsToolkit
{
[OpenApi(Prefix = "stripe")]
[RequiresPermission]
public OpenApiConfig Stripe() => new()
{
SpecPath = "stripe.json",
AuthCallback = ...
};
}Custom error detection (Slack-style)
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:
| Status | Behavior |
|---|---|
429, 401, 408, 5xx | Thrown as OpenApiRequestException → caught by FunctionRetryMiddleware, retried with backoff |
400, 404, 422, other 4xx | Returned to the LLM as an error description so it can self-correct its request |
Next Steps
- 02.1.1 AIFunctions.md — native C# tool functions
- 02.1.5 Context Engineering.md — collapsing and container nesting
- 02.2 MCP Servers.md — another way to connect external tools