Skip to content
NerdSnipe-IncPublic

About

A Swift package that gives every app a single, unified chat interface across cloud and on-device AI providers. OpenAI, Anthropic, and Apple Intelligence all share the same protocol, message model, and optional SwiftUI components — swap providers with a one-line change.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Repository files navigation

AIChatKit

Swift 5.10 iOS 17+ macOS 14+ MIT License SPM

A Swift package that gives every app a single, unified chat interface across cloud and on-device AI providers. OpenAI, Anthropic, and Apple Intelligence all share the same protocol, message model, and optional SwiftUI components — swap providers with a one-line change.

Platforms: macOS 14+ · iOS 17+
Language: Swift 5.10+

For on-device inference, add a companion package:

  • AIChatKitMLX — Apple MLX models (Apple Silicon only, text + vision)
  • AIChatKitLlama — any GGUF model via llama.cpp, in-process on Metal

Sponsor NerdSnipe-Inc

AIChatKit is free and open source. If it saved you time, sponsoring NerdSnipe Inc pays for the maintenance, bug fixes and new releases that keep it working.

Products

Product Description
AIChatCore Protocol, message model, shared types
AIChatOpenAI OpenAI-compatible provider (OpenAI, OpenRouter, llama-server, …)
AIChatAnthropic Anthropic Messages API with extended thinking support
AIChatFoundationModels Apple Intelligence on-device (macOS 26+ / iOS 26+)
AIChatUI ChatSession ViewModel + MarkdownMessageView + optional ChatView drop-in

Installation

// Package.swift
.package(url: "https://github.com/NerdSnipe-Inc/AIChatKit", from: "2.0.0")

// Target dependencies — add only what you need
.product(name: "AIChatCore",             package: "AIChatKit"),
.product(name: "AIChatOpenAI",           package: "AIChatKit"),
.product(name: "AIChatAnthropic",        package: "AIChatKit"),
.product(name: "AIChatFoundationModels", package: "AIChatKit"),
.product(name: "AIChatUI",               package: "AIChatKit"),

Quick start — custom UI (recommended)

Most apps have their own design system. Use ChatSession as the ViewModel and render session.entries in your own views:

import AIChatAnthropic
import AIChatUI

@StateObject private var session = ChatSession(
    provider: AnthropicProvider(apiKey: "sk-ant-…"),
    model: "claude-opus-4-8",
    options: ChatRequestOptions(
        maxTokens: 4096,
        systemPrompt: "You are a helpful assistant."
    )
)

var body: some View {
    ScrollView {
        ForEach(session.entries) { entry in
            switch entry {
            case .userMessage(let e):   MyUserBubble(text: e.text)
            case .aiMessage(let e):     MyAssistantBubble(text: e.text, isStreaming: e.isStreaming)
            case .reasoning(let e):     MyThinkingTile(entry: e)
            case .toolCall(let e):      MyToolCallRow(entry: e)
            case .activity(let e):      Text(e.text).foregroundStyle(.secondary)
            case .knowledgeRetrieval:   EmptyView()
            }
        }
    }
    TextField("Message", text: $input)
        .onSubmit { session.send(input); input = "" }
}

session.send(_:) is non-blocking. Entries append as tokens arrive. session.isGenerating tracks in-flight state.


Drop-in UI

If you have no design requirements, ChatView handles everything:

import AIChatUI
import AIChatOpenAI

ChatView(session: ChatSession(
    provider: OpenAIProvider(apiKey: "sk-…"),
    model: "gpt-4o"
))

Markdown rendering

AIChatUI exports MarkdownMessageView — a fully styled markdown renderer built on swift-markdown-ui and Splash. It handles fenced code blocks with syntax highlighting, a language label, and a copy button.

import AIChatUI

// In a custom AI bubble:
case .aiMessage(let e):
    MarkdownMessageView(text: e.text)

Supports: headings, bold/italic, inline code, fenced code blocks (syntax-highlighted), blockquotes, lists, tables, links.


Restoring saved conversations

Use loadSnapshot(entries:history:) to restore a persisted conversation into both the display entries and provider history in a single call:

var snapshotEntries: [ChatSession.Entry] = []
var snapshotHistory: [ChatMessage] = []

for msg in storedMessages {
    if msg.role == "user" {
        snapshotEntries.append(.userMessage(ChatSession.UserEntry(id: msg.id, text: msg.content)))
        snapshotHistory.append(ChatMessage(id: msg.id, role: .user, content: msg.content))
    } else {
        snapshotEntries.append(.aiMessage(ChatSession.AIEntry(id: msg.id, text: msg.content, isStreaming: false)))
        snapshotHistory.append(ChatMessage(id: msg.id, role: .assistant, content: msg.content))
    }
}

session.loadSnapshot(entries: snapshotEntries, history: snapshotHistory)
// session.send() now has full history context

Providers

OpenAI

import AIChatOpenAI

let provider = OpenAIProvider(apiKey: "sk-…")

// OpenAI-compatible endpoint (OpenRouter, llama-server, Groq, …)
let provider = OpenAIProvider(
    apiKey: "your-key",
    endpoint: URL(string: "https://openrouter.ai/api/v1/chat/completions")!
)

// Local llama-server — disable stream_options
let provider = OpenAIProvider(apiKey: "", endpoint: llamaURL, streamUsage: false)

Anthropic

import AIChatAnthropic

let provider = AnthropicProvider(apiKey: "sk-ant-…")

// Extended thinking
ChatRequestOptions(maxTokens: 16000, thinkingBudget: 8000)

Apple Intelligence (Foundation Models)

import AIChatFoundationModels
import FoundationModels

@available(macOS 26.0, iOS 26.0, *)
guard case .available = SystemLanguageModel.default.availability else { return }
let provider = FoundationModelsProvider()

Context window is 4,096 tokens on-device. When the conversation exceeds this limit, FoundationModelsProvider surfaces LanguageModelSession.GenerationError.exceededContextWindowSize. Callers should catch this and either summarize history or switch to a higher-context provider (e.g. MLXProvider from AIChatKitMLX, or PrivateCloudComputeLanguageModel when it becomes available in the public SDK).

session.$error
    .sink { error in
        if let genError = error as? LanguageModelSession.GenerationError,
           case .exceededContextWindowSize = genError {
            // switch to higher-context provider and restore history
        }
    }

Tool use

import AIChatCore

let tool = ChatRequestOptions.ToolDefinition(
    name: "get_weather",
    description: "Get the current weather for a city.",
    parameters: .object(
        properties: ["city": .string(description: "City name")],
        required: ["city"]
    )
)

ChatRequestOptions(tools: [tool], toolChoice: .auto)

When a .toolCall entry has status == .running, execute the tool and call:

session.submitToolResult(toolCallId: entry.id, content: resultString)

Error handling

ChatSession surfaces errors as .activity entries automatically. For custom error UI:

.onChange(of: session.error) { _, error in
    guard let error else { return }
    // show your own alert
}

Upgrade notes

  • 2.0.0 removed the Gemma text-parsing API. GemmaOutputRecovery, GemmaToolArguments and EmbeddedToolCallParser are gone, and ChatSession no longer parses tool calls out of assistant text. A provider now turns model-specific text calls into .toolCallComplete events itself; AIChatKitMLX 1.4.0+ does this for Gemma (call:, <tool_call> XML, tool_code). Other providers are unaffected. UserEntry.isFailed is new.
  • ChatError grew in 1.1.0. The local-model cases (modelNotFound, modelDownloadFailed, outOfMemory, modelLoadFailed, unsupportedModel, templateError, toolCallParseFailed, generationFailed) were added, so an exhaustive switch over ChatError needs a default:. ChatSession.send now returns a Bool (@discardableResult; false when the text is empty or the session is busy).
  • Unanswered turns are not resent. A turn cancelled, failed, or answered with nothing before any output is dropped from provider history and marked UserEntry.isCancelled / UserEntry.isFailed, so the provider always sees strict user/assistant alternation. It stays in the transcript; resend it to retry.

Details: docs/CHAT_SESSION_BEHAVIOUR.md.


License

MIT

Support this project

AIChatKit is built and maintained by NerdSnipe Inc, a small independent studio in Ottawa. Sponsorship funds keeping up with Apple, OpenAI and Anthropic API changes.

About

A Swift package that gives every app a single, unified chat interface across cloud and on-device AI providers. OpenAI, Anthropic, and Apple Intelligence all share the same protocol, message model, and optional SwiftUI components — swap providers with a one-line change.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages