Skip to content

Tools

toolSearch(maxResults:search:) can rank deferred tools using an asynchronous callback. The callback sees eligible candidates; returned names are filtered against that set, deduplicated, and capped. Discovered tools become available on the next model step. A positive maxResults is required:

let search = try toolSearch(maxResults: 3) { query, candidates in
candidates.filter {
$0.name.localizedCaseInsensitiveContains(query)
}.map(\.name)
}

Generic compatible servers may opt in to rich tool outputs through AIProviders.openAICompatible(..., supportsMultiPartToolContent: true) or ProviderSettings.supportsMultiPartToolContent = true. The default string representation is retained; opt in only when the server accepts content arrays.

Tools let a model ask your Swift code to do discrete work, then continue with the tool result.

Examples/Sources/Tools/main.swift
import SwiftAISDK
@main
struct ToolsExample {
static func main() async throws {
let weather = AITool(
name: "weather",
description: "Get the current weather in a city.",
parameters: [
"type": "object",
"properties": [
"city": ["type": "string"],
],
"required": ["city"],
]
) { arguments in
let city = arguments["city"]?.stringValue ?? "unknown"
return [
"city": .string(city),
"forecast": "sunny",
"temperatureCelsius": 24,
]
}
let provider = try AIProviders.openAI()
let model = try provider.languageModel("gpt-4.1-mini")
let answer = try await model.generateText(
"What should I wear in Tokyo today?",
tools: LanguageToolOptions([weather], maxSteps: 3)
)
print(answer.text)
}
}

An AITool has:

  • a stable name
  • an optional description
  • a JSON Schema parameters value
  • an async execute function
  • optional strict mode, provider metadata, provider options, and input examples
  • optional argument refinement, approval policy, context schema, streamed-input callbacks, and model-facing output mapping

Use maxSteps to let the model call tools, receive results, and make a follow-up model call until it returns a final answer or the step limit is reached.

Tool-enabled generateText and streamText can be customized with LanguageToolOptions:

let options = LanguageToolOptions(
[weather],
maxSteps: 5,
stopWhen: [.hasToolCall("weather")],
prepareStep: { context in
AIPrepareStepResult(request: context.request)
}
)
let result = try await model.generateText(
"Plan my afternoon.",
tools: options
)

Each tool-loop result records steps, toolCalls, toolResults, approval parts, and responseMessages so you can inspect what happened or continue the conversation.

Tool choice is enforced at the facade boundary. toolChoice: none rejects returned tool calls before repair, refinement or execution callbacks; streaming tool parts are withheld from consumers. If required, or a named tool, completes without the required call, generation throws AIToolChoiceViolationError. An interrupted response retains its partial answer and skips this missing-call check, tool execution and automatic continuation. The error retains the requested choice, provider/model identity, finish reason and returned content.

Set LanguageModelRequest.toolCallConcurrency to a positive integer to cap local tool executions within a step. 1 executes calls in the model’s order; nil runs them concurrently. Results retain model order even when executions finish in a different order. Queued calls do not start after cancellation or an execution failure. The same limit applies to restored approvals and streaming tool loops. AIChatRequestOptions.toolCallConcurrency carries this setting through native agents and chat transports.

var request = LanguageModelRequest(messages: [.user("Check these records.")])
request.toolCallConcurrency = 1
let result = try await AI.generateText(
model: model, request: request, executableTools: tools
)

Approved history is scheduled in original model order even when approvals arrive out of order. An approval alone does not replace a local tool result in a direct model prompt; use the executable-tool facade to run approved calls first.

For repeated or conflicting restored approval responses, the first response for each tool call wins, including different approval IDs referring to the same call.

Use needsApproval on a tool when a specific tool call should pause for approval, or use a call-level approval policy when one central policy should evaluate every tool call.

let transfer = AITool(
name: "transfer_funds",
description: "Transfer money between accounts.",
parameters: [
"type": "object",
"properties": ["amount": ["type": "number"]],
"required": ["amount"],
],
needsApproval: { input, _ in
input["amount"]?.numberValue ?? 0 > 100
}
) { input in
["status": "queued", "amount": input["amount"] ?? .null]
}

Approval requests and responses are represented as typed result and stream parts. When you use signed approval replay, pass a toolApprovalSecret so approvals from persisted messages can be verified before a tool is executed. Invalid or tampered signatures fail with AIInvalidToolApprovalSignatureError.

An approval policy can return context.userApproval(reason: "...") to preserve a human-readable justification on AIToolApprovalRequest.reason and in telemetry. Returning .userApproval remains the reason-free shorthand. If an approved persisted call no longer validates against the current tool schema, SwiftAISDK does not execute it; the model receives a tool-error result and can continue the turn safely.

When refineArguments changes a tool input, AIToolApprovalRequest stores the schema-validated original value in inputSchemaInput and the refined value in arguments. Approval replay validates the original value again, reruns the refinement, and rejects tampering before execution. Persist both fields with the approval request; do not reconstruct one from the other.

Approval continuation also searches earlier assistant messages, not only the last message. A persisted approval response can therefore resume its matching tool call after later conversation messages without executing the approved operation twice.

AIToolApprovalRequest.descriptor retains opaque provider-computed display information through validation and telemetry. Treat it as untrusted UI metadata, not as authorization evidence. A failed tool result’s provider metadata is also restored onto its associated model-facing call when a provider needs a namespace or replay identifier.

Tools can observe streamed arguments before execution:

let search = AITool(
name: "search",
parameters: ["type": "object"],
onInputDelta: { context in
print("tool input delta:", context.inputTextDelta)
}
) { input in
["results": []]
}

Use onInputStart, onInputDelta, and onInputAvailable to update a UI as a model is still producing arguments. These callbacks receive the tool call ID, request messages, abort signal, and per-tool context.

Set toModelOutput when the value your app stores should differ from the value sent back to the model. The public AIToolResult keeps both result and optional modelOutput.

Use dynamic tools when the exact schema is discovered at runtime, such as tools loaded from MCP.

let dynamicTool = AITool.dynamic(
name: "runtime_search",
description: "Search a runtime index.",
parameters: ["type": "object"]
) { arguments in
["ok": true, "input": arguments]
}

experimentalToolCaller can route a tool through a local caller or provider caller. A local-caller-only target is omitted from the model-facing tool list; the bound caller is advertised and remains executable. If a provider emits the hidden target name directly anyway, SwiftAISDK returns a tool-error result and does not run the target.

Provider callers instead keep the tool model-visible and rewrite its provider options before the request.

OpenAI 4.0.43 allowedTools limits the tools that Responses may choose while still sending the complete request tool set. Declare tool names in the OpenAI provider options (or the equivalent extraBody shape):

let request = LanguageModelRequest(
messages: [.user("Search the docs, but do not edit anything.")],
tools: [
"docs": OpenAITools.mcp(
serverLabel: "docs",
serverURL: "https://mcp.example.com"
),
"computer": OpenAITools.computer()
],
providerOptions: [
"openai": [
"allowedTools": [
"toolNames": ["docs", "computer"],
"mode": "required"
]
]
]
)

Function tools, supported hosted tools, labeled MCP servers, custom tools, and OpenAITools.computer() map to tool_choice.allowed_tools. Deferred, namespaced, tool-search, and legacy computerUse(...) entries warn and are removed; an allow-list containing only unrepresentable entries is rejected. The current computer tool is distinct from the older computer_use wire shape.

OpenAITools.imageGeneration(action:) and AzureOpenAITools.imageGeneration(action:) expose the current hosted image-tool action alongside background, fidelity, mask, model, output, quality, and size options.

The generic Open Responses 2.0.54 provider does not inherit OpenAI’s allowedTools hosted-tool allow-list. It does support its own custom-tool surface through OpenResponsesTools.customTool(...) and OpenResponsesTools.textFormat(), including generated and streamed custom tool calls and results.

Text-encoded tool input uses .toolInputStart(id:name:inputEncoding: .text) and emits the .toolInputStartText companion case. The default remains JSON. Native UI reducers concatenate raw text while keeping final AIToolCall.arguments JSON-encoded, including string inputs. AIToolCall.inputEncoding and rawInputText preserve unfinished text input across UI snapshot restoration.