Tools
Configurable tool discovery
Section titled “Configurable tool discovery”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.
import SwiftAISDK
@mainstruct 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) }}Tool Shape
Section titled “Tool Shape”An AITool has:
- a stable
name - an optional
description - a JSON Schema
parametersvalue - an async
executefunction - optional
strictmode, 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 Loops
Section titled “Tool Loops”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.
Tool execution concurrency
Section titled “Tool execution concurrency”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 = 1let 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.
Approvals
Section titled “Approvals”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.
Streamed Tool Input
Section titled “Streamed Tool Input”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.
Model-Facing Output
Section titled “Model-Facing Output”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.
Dynamic Tools
Section titled “Dynamic Tools”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]}Tool Callers
Section titled “Tool Callers”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 Responses Allow Lists
Section titled “OpenAI Responses Allow Lists”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.