Contents

nerdsnipe-inc/aichatkit

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.

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"),

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.

Package Metadata

Repository: nerdsnipe-inc/aichatkit

Default branch: main

README: README.md