Swift

Swift has no first-party OpenAI SDK. Most projects use MacPaw’s OpenAI package, which is community-maintained; the rest call the HTTP API directly with URLSession. Either way you change the same two things: the host you talk to and the key you send. Every ChatQuery you already build stays exactly as it is.

Install

MacPaw OpenAI (community)
// Package.swift
.package(url: "https://github.com/MacPaw/OpenAI.git", from: "0.4.0")
URLSession
Nothing to install — URLSession is in Foundation.

Configure

MacPaw OpenAI (community)

The package builds its URLs from a Configuration, and appends /v1/chat/completions to the basePath itself. So the gateway goes in as a host plus a base path — and your queries do not change at all.

import OpenAI

let org = ProcessInfo.processInfo.environment["VULNETIX_ORG_UUID"]!

let configuration = OpenAI.Configuration(
    token: ProcessInfo.processInfo.environment["VULNETIX_API_KEY"]!, // Vulnetix key
    host: "guardrails.vulnetix.com",
    basePath: "/openai/\(org)"
)

let openAI = OpenAI(configuration: configuration)

// Every call you already make is unchanged:
let result = try await openAI.chats(query: query)

The final URL must come out as https://guardrails.vulnetix.com/openai/{orgUuid}/v1/chat/completions. If your version of the package composes paths differently, adjust basePath until it does — that URL is the contract, not the field.

URLSession
import Foundation

let org = ProcessInfo.processInfo.environment["VULNETIX_ORG_UUID"]!
let key = ProcessInfo.processInfo.environment["VULNETIX_API_KEY"]!

var request = URLRequest(
    url: URL(string: "https://guardrails.vulnetix.com/openai/\(org)/v1/chat/completions")!
)
request.httpMethod = "POST"
request.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: [
    "model": "gpt-4o-mini",
    "messages": [["role": "user", "content": "Hello"]],
])

let (data, response) = try await URLSession.shared.data(for: request)

The token is your Vulnetix API key, not a provider key. Swap openai in the path for any other provider slug; model names pass through verbatim.

Streaming

Unchanged. The gateway relays the provider’s SSE stream — with MacPaw’s package, openAI.chatsStream(query:) works exactly as before.

Handling a guardrail block

A request your policy refuses never reaches the provider. It returns an OpenAI-shaped 403, and the two fields worth reading are code and blocked_by.

struct FirewallErrorEnvelope: Decodable {
    struct FirewallError: Decodable {
        let code: String?
        let message: String?
        let blocked_by: String?
    }
    let error: FirewallError
}

let (data, response) = try await URLSession.shared.data(for: request)

if let http = response as? HTTPURLResponse, http.statusCode == 403 {
    let envelope = try JSONDecoder().decode(FirewallErrorEnvelope.self, from: data)
    switch envelope.error.code {
    case "request_blocked":
        throw AppError.firewallBlocked(rule: envelope.error.blocked_by ?? "unknown")
    default:
        // provider_denied, model_denied, model_not_allowed, provider_key_missing
        throw AppError.policyRefused(code: envelope.error.code ?? "unknown")
    }
}
codeMeaning
request_blockedA content guardrail matched. blocked_by names the rule.
provider_deniedPolicy does not allow this provider.
model_denied / model_not_allowedPolicy does not allow this model.
provider_key_missingNo provider key in the vault for this provider.

With MacPaw’s package the failure surfaces as its own API error type, which models the standard OpenAI error fields — code is there, blocked_by is not, because it is a Vulnetix addition the package does not know about. The rule name is also in the message text; decode the body yourself, as above, if you want it structured.

Note Redaction is silent — you get a 200. A pii_redact rule rewrites the matched spans in your prompt to the literal [REDACTED] and forwards the request. There is no error to catch; the completion simply talks about redacted text.

Tool / function calling

Tools are forwarded untouched. Guardrails inspect the request you send, which includes the tool-result messages you send back on the next turn.

Gotchas

  • Do not ship the Vulnetix key in an iOS or macOS app. A key in a client binary is a key you have published, and the firewall cannot help with a credential an attacker already holds. Call the gateway from your own backend and let the app talk to that.
  • MacPaw’s Configuration splits the URL into host + basePath and adds /v1/… itself — so unlike most SDKs you do not put /v1 in what you configure.
  • The token is the Vulnetix key. A provider key here is the most common 401.