Advanced

Middleware, MCP tools, uploads, telemetry, errors, and retries.

Middleware

wrapLanguageModel intercepts requests and stream parts. The built-ins cover the common cases:

let model = wrapLanguageModel(
  model: OllamaModel("qwen3"),
  middleware: [
    .cache(),                          // replay identical requests
    .extractReasoning(tag: "think"),   // lift <think> spans into reasoning
    .defaultSettings(temperature: 0.2) // bake in defaults
  ]
)

.simulateStreaming() turns a non-streaming endpoint into a streaming one.

Caching

.cache() keys on the request plus the wrapped model's identity. A hit replays the stored stream parts without calling the model; a miss streams live, buffers the parts, and stores them once the stream completes. Errors are never cached.

let store = InMemoryLanguageModelCache()
let model = wrapLanguageModel(model: OpenAIModel("gpt-5.6-luna"), middleware: [.cache(store: store)])

The default store is in-process. Conform to LanguageModelCache (get/set over [StreamPart]) to back it with Redis, disk, or anything else.

Custom middleware

A middleware is a value with the hooks you need — transformRequest (edit the request), wrapStream (post-process stream parts), or wrapCall (wrap the whole call, deciding whether to invoke the model at all — this is what .cache() uses):

let logger = LanguageModelMiddleware(
  transformRequest: { request in
    print("sending \(request.messages.count) messages")
    return request
  }
)

Middlewares apply in array order.

MCP tools

MCPClient speaks Streamable HTTP (JSON-RPC over POST, SSE responses, session ids) and bridges server tools straight into the loop:

let mcp = MCPClient(transport: MCPHTTPTransport(url: serverURL))
try await mcp.connect()

let result = try await generateText(
  model: model,
  prompt: "What is in my calendar today?",
  tools: try await mcp.tools()
)

Bridged tools validate arguments against the server's schemas and return results into the same loop as local tools.

File and skill uploads

See Files and skills for the full OpenAI Files, Anthropic Files, and Anthropic Skills APIs.

// OpenAI Files API
let file = try await OpenAIFiles().upload(
  data: pdf, filename: "report.pdf", purpose: "user_data"
)

// Anthropic Files API (beta headers handled)
let upload = try await AnthropicFiles().upload(
  data: pdf, filename: "report.pdf", mediaType: "application/pdf"
)

// Anthropic Skills: a SKILL.md folder, zipped into a reusable skill
let skill = try await AnthropicSkills().upload(
  files: [SkillFile(path: "brand-guide/SKILL.md", data: skillMD)],
  displayTitle: "Brand guide"
)

Telemetry

AITelemetry is a process-wide hook, disabled until you set a collector. Conform to AITelemetryCollector and forward events into OSLog signposts, an OpenTelemetry exporter, or your analytics:

struct LogCollector: AITelemetryCollector {
  func record(_ event: AITelemetryEvent) {
    logger.info("\(event.name) \(event.phase)", metadata: [
      "model": "\(event.attributes["ai.model.id"] ?? "")",
      "duration": "\(event.duration)"
    ])
  }
}
AITelemetry.collector = LogCollector()

Each call is bracketed by start and end (or error) phases with a measured duration. Spans cover ai.generateText, ai.streamText, ai.generateObject, and the embedding calls, with model, usage, and finish-reason attributes.

Errors

Failures surface as typed AIError cases:

do {
  let result = try await generateText(model: model, prompt: prompt)
} catch let AIError.http(status, body) {
  // 4xx/5xx with the provider's error body
} catch AIError.noObjectGenerated {
  // structured output did not parse or validate
} catch {
  // transport errors, cancellation
}

Other cases include .decoding, .invalidRequest, and .unknownTool. Streaming surfaces errors by throwing from the stream you iterate.

Retries

Every request path retries transient failures with exponential backoff. maxRetries (default 2) counts retries after the first attempt; set 0 to disable:

let result = try await generateText(
  model: model,
  prompt: prompt,
  maxRetries: 4
)

Establishing a stream is retryable; a stream that already delivered parts is not, so you never see duplicated tokens.