macpaw/openai

repository·main·Indexed 25 days ago

https://github.com/macpaw/openai

A Swift community-maintained implementation of the OpenAI public API. It enables integration of OpenAI features and other OpenAI-compatible providers (such as Gemini, DeepSeek, Perplexity, or OpenRouter) into Swift applications. The SDK supports Chat Completions, the Responses API, image generation and editing, text-to-speech, audio transcription, and structured outputs via JSON Schema.

Tokens
7.8K
Snippets
16
Records
31
Agent score
34%

What's inside macpaw-openai

  1. Configure Structured Outputs via JSON Schema

    main

    You can enforce structured JSON outputs by defining a JSON Schema and passing it to your query. The method of passing the schema depends on the query type:

    • ChatQuery (Chat Completions API): Use the responseFormat: .jsonSchema(...) parameter.
    • CreateModelResponseQuery (Responses API): Use the text: .jsonSchema(...) parameter.

    Both methods accept a JSONSchemaDefinition value. The SDK provides three ways to define these schemas: specifying fields manually, deriving them from Swift types, or using dynamic dictionaries.

  2. Handle MCP Tool Calls and Approvals

    main

    When using MCP tools, the model may generate tool calls that are executed on the remote server. You must handle MCP-specific output items in your response processing, specifically .mcpToolCall (to access the tool name and output) and .mcpApprovalRequest (if requireApproval is enabled).

    // Handle MCP tool calls in streaming responses
    for try await result in openAI.chatsStream(query: query) {
        for choice in result.choices {
            if let outputItem = choice.delta.content {
                switch outputItem {
                case .mcpToolCall(let mcpCall):
                    print("MCP tool call: \(mcpCall.name)")
                    if let output = mcpCall.output {
                        print("Result: \(output)")
                    }
                case .mcpApprovalRequest(let approvalRequest):
                    // Handle approval request if requireApproval is enabled
                    print("MCP tool requires approval: \(approvalRequest)")
                default:
                    // Handle other output types
                    break
                }
            }
        }
    }
  3. Integrate Remote MCP (Model Context Protocol) Tools

    main

    The SDK supports the Model Context Protocol (MCP), allowing models to connect to external data sources and tools via standardized server connections. You can use the MCP Swift library to discover tools and then integrate them into an OpenAI ChatQuery using Tool.mcpTool.

    MCP Tool Configuration

    When creating an mcpTool, you can specify:

    • serverLabel: A unique identifier for the MCP server.
    • serverUrl: The URL endpoint of the MCP server.
    • headers: Authentication and other HTTP headers.
    • allowedTools: An optional list of specific tools to enable from the server.
    • requireApproval: Determines if tool calls require user approval (.always, .never, or conditional).
    // Create an MCP tool for connecting to a remote server
    let mcpTool = Tool.mcpTool(
        .init(
            _type: .mcp,
            serverLabel: "GitHub_MCP_Server",
            serverUrl: "https://api.githubcopilot.com/mcp/",
            headers: .init(additionalProperties: [
                "Authorization": "Bearer YOUR_TOKEN_HERE"
            ]),
            allowedTools: .case1(["search_repositories", "get_file_contents"]),
            requireApproval: .case2(.always)
        )
    )
    
    let query = ChatQuery(
        messages: [
            .user(.init(content: .string("Search for Swift repositories on GitHub")))
        ],
        model: .gpt4_o,
        tools: [mcpTool]
    )
  4. Use the SDK with other providers (Gemini, Perplexity, etc.)

    main

    While optimized for OpenAI, this SDK supports other providers by using specific parsingOptions in the OpenAI.Configuration. This allows the SDK to handle responses that do not strictly adhere to the OpenAI API schema.

    Option 1: Relaxed Parsing

    Use the .relaxed parsing option to handle both missing keys and additional key/value pairs in responses. This is the recommended approach for most non-OpenAI use cases.

    Option 2: Specific Parsing Options

    If .relaxed is insufficient, you can target specific schema discrepancies:

    • Handle missing keys: If a provider omits a field that OpenAI considers required (e.g., Gemini omitting the id field), use .fillRequiredFieldIfKeyNotFound.
    • Handle missing values: If a provider returns null for a field that OpenAI requires to be non-optional, use .fillRequiredFieldIfValueNotFound.

    Supported Additional Fields

    The SDK automatically maps certain provider-specific fields into the main model set:

    • ChatResult.citations: Supported by Perplexity.
    • ChatResult.Choice.Message.reasoningContent: Supported by Grok and DeepSeek.
    • ChatResult.Choice.Message.reasoning: Supported by OpenRouter.
  5. Install OpenAI via Swift Package Manager

    main

    You can integrate the OpenAI SDK into your Xcode project using Swift Package Manager (SPM) in two ways:

    Via Xcode UI:

    1. Go to File > Add Package Dependencies...
    2. Enter the repository URL: https://github.com/MacPaw/OpenAI.git
    3. Choose your dependency rule (e.g., "Up to Next Major Version").

    Via Package.swift: Add the package directly to your dependencies array.

    dependencies: [
        .package(url: "https://github.com/MacPaw/OpenAI.git", branch: "main")
    ]
  6. Initialize the OpenAI client

    main

    To use the SDK, initialize the OpenAI class with your API token.

    Security Warning: Do not expose your API key in client-side code. It is strongly recommended to proxy requests through a backend server to keep your key secure.

    Basic initialization requires only the apiToken. For advanced configuration, use OpenAI.Configuration to specify an organizationIdentifier, timeoutInterval, host, basePath, port, scheme, or customHeaders.

    // Basic initialization
    let openAI = OpenAI(apiToken: "YOUR_TOKEN_HERE")
    
    // Advanced configuration
    let configuration = OpenAI.Configuration(token: "YOUR_TOKEN_HERE", organizationIdentifier: "YOUR_ORGANIZATION_ID_HERE", timeoutInterval: 60.0)
    let openAI = OpenAI(configuration: configuration)
  7. Derive JSON Schema from a Swift type

    main

    You can define schemas by implementing the JSONSchemaConvertible protocol on your Swift types (similar to Pydantic or Zod).

    Requirements:

    1. The type must conform to JSONSchemaConvertible and provide a static let example: Self property.
    2. All enum types within the provided type must conform to JSONSchemaEnumConvertible and implement var caseNames: [String] { get } to return an array of all case names.
    struct MovieInfo: JSONSchemaConvertible {
        
        let title: String
        let director: String
        let release: Date
        let genres: [MovieGenre]
        let cast: [String]
        
        static let example: Self = {
            .init(
                title: "Earth",
                director: "Alexander Dovzhenko",
                release: Calendar.current.date(from: DateComponents(year: 1930, month: 4, day: 1))!,
                genres: [.drama],
                cast: ["Stepan Shkurat", "Semyon Svashenko", "Yuliya Solntseva"]
            )
        }()
    }
    enum MovieGenre: String, Codable, JSONSchemaEnumConvertible {
        case action, drama, comedy, scifi
        
        var caseNames: [String] { Self.allCases.map { $0.rawValue } }
    }
    let query = ChatQuery(
        messages: [
            .system(
                .init(content: .textContent("Best Picture winner at the 2011 Oscars"))
            )
        ],
        model: .gpt4_o,
        responseFormat: .jsonSchema(
            .init(
                name: "movie-info",
                description: nil,
                schema: .derivedJsonSchema(MovieInfo.self),
                strict: true
            )
        )
    )
    let result = try await openAI.chats(query: query)
  8. Cancel requests

    main

    The SDK supports cancellation across different concurrency models:

    Swift Concurrency (async/await): Simply cancel the parent Task. The underlying URLSessionDataTask will be cancelled automatically.

    Closure-based API calls: These methods return a CancellableRequest. Hold a reference to this object to cancel the request manually.

    Combine subscriptions: Use standard Combine cancellation by calling .cancel() on the subscription object or by discarding the reference.

    // Swift Concurrency cancellation
    let task = Task {
        do {
            let chatResult = try await openAIClient.chats(query: .init(messages: [], model: "asd"))
        } catch {
            // Handle cancellation or error
        }
    }
                
    task.cancel()
  9. Use the SDK with other OpenAI-compatible providers

    main
    While optimized for the OpenAI platform, this SDK supports other providers (such as Gemini, DeepSeek, Perplexity, or OpenRouter) that implement an OpenAI-compatible API. To ensure compatibility with non-OpenAI responses, use the .relaxed parsing option in your OpenAI.Configuration.
  10. Define a dynamic JSON Schema using Encodable

    main

    You can define a JSON schema using simple Dictionaries or a schema library by wrapping the dictionary in an AnyEncodable type that conforms to Encodable. This allows you to pass a dynamic schema via .derivedJsonSchema (or similar) to the responseFormat or text parameters.

    struct AnyEncodable: Encodable {
        private let _encode: (Encoder) throws -> Void
        public init<T: Encodable>(_ wrapped: T) {
            _encode = wrapped.encode
        }
        func encode(to encoder: Encoder) throws {
            try _encode(encoder)
        }
    }
    let schema = [
        "type": AnyEncodable("object"),
        "properties": AnyEncodable([
            "title": AnyEncodable([
                "type": AnyEncodable("string")
            ]),
            "director": AnyEncodable([
                "type": AnyEncodable("string")
            ]),
            "release": AnyEncodable([
                "type": AnyEncodable("string")
            ]),
            "genres": AnyEncodable([
                "type": AnyEncodable("array"),
                "items": AnyEncodable([
                    "type": AnyEncodable("string"),
                    "enum": AnyEncodable(["action", "drama", "comedy", "scifi"])
                ])
            ]),
            "cast": AnyEncodable([
                "type": AnyEncodable("array"),
                "items": AnyEncodable([
                    "type": AnyEncodable("string")
                ])
            ])
        ]),
        "additionalProperties": AnyEncodable(false)
    ]
    let query = ChatQuery(messages: [.system(.init(content: .textContent("Return a structured response.")))], model: .gpt4_o, responseFormat: .jsonSchema(.init(name: "movie-info", schema: .dynamicJsonSchema(schema))))
    let result = try await openAI.chats(query: query)
  11. Build a schema by specifying fields manually

    main

    You can build a schema in a type-safe manner using initializers that accept [JSONSchemaField]. This is a direct but more verbose method of defining a schema structure.

    let query = CreateModelResponseQuery(
        input: .textInput("Return structured output"),
        model: .gpt4_o,
        text: .jsonSchema(.init(
            name: "research_paper_extraction",
            schema: .jsonSchema(.init(
                .type(.object),
                .properties([
                    "title": Schema.buildBlock(
                        .type(.string)
                    ),
                    "authors": .init(
                        .type(.array),
                        .items(.init(
                            .type(.string)
                        ))
                    ),
                    "abstract": .init(
                        .type(.string)
                    ),
                    "keywords": .init(
                        .type(.array),
                        .items(.init(
                            .type(.string)
                        ))
                    )
                ]),
                .required(["title, authors, abstract, keywords"]),
                .additionalProperties(.boolean(false))
            )),
            description: "desc",
            strict: false
        ))
    )
    
    let response = try await openAIClient.responses.createResponse(query: query)
    for output in response.output {
        switch output {
        case .outputMessage(let message):
            for content in message.content {
                switch content {
                case .OutputTextContent(let textContent):
                    print("json output structured by the schema: ", textContent.text)
                case .RefusalContent(let refusal):
                    // Handle refusal
                    break
                }
            }
        default:
            // Handle other OutputItems
            break
        }
    }
  12. Implement Function Calling with Chat Completions

    main

    You can enable function calling by defining tools within a ChatQuery. Use ChatQuery.ChatCompletionToolParam.FunctionDefinition to describe your functions, including their name, description, and JSON Schema parameters. Pass these tools to the tools argument in the ChatQuery initializer. When the model decides to call a function, the toolCalls property on the message will be populated.

    let openAI = OpenAI(apiToken: "...")
    
    // Declare functions which model might decide to call.
    let functions = [
        ChatQuery.ChatCompletionToolParam.FunctionDefinition(
            name: "get_weather",
            description: "Get current temperature for a given location.",
            parameters: .init(fields: [
                .type(.object),
                .properties([
                    "location": .init(fields: [
                        .type(.string),
                        .description("City and country e.g. Bogotá, Colombia")
                    ])
                ]),
                .required(["location"]),
                .additionalProperties(.boolean(false))
            ])
        )
    ]
    
    let query = ChatQuery(
        messages: [
            .user(.init(content: .string("What is the weather like in Paris today?")))
        ],
        model: .gpt4_1,
        tools: functions.map { .init(function: $0) }
    )
    
    let result = try await openAI.chats(query: query)
    print(result.choices[0].message.toolCalls)