Skip to content

Repository files navigation

Sophon

Swift Platforms Release License

Gemini client for Swift. Sophon is a Swift Package for iOS 18+, macOS 15+, and Mac Catalyst that wraps the Google Gemini API with schema-constrained structured output, configurable retry policies, a model catalog that falls back automatically when Google retires a model, and lenient decoding for the JSON that LLMs actually return. The request/retry/decoding kernel was extracted from three production iOS apps that each carried it as copy-pasted code, and it ships in all three today (see Used in). Gemini is the only provider so far; further providers land as sibling targets on the same core.

智子, the proton-sized intelligence from The Three-Body Problem: it observes and reports.

Targets

Product Depends on Contents
SophonCore nothing LLMDecoding (lenient LLM JSON decoding), LLMJSONExtractor (fence stripping, brace extraction, truncation repair), SophonKeychain, SophonLogger (pluggable log handler)
SophonGemini SophonCore GeminiAPIClient, GeminiClientConfiguration, GeminiRetryPolicy, request/response DTOs, GeminiSchema (structured output), GeminiModel catalog + GeminiModelStore, GeminiError, availability/key helpers, UIKit-gated image encoding

Platforms: iOS 18+, Mac Catalyst 18+, macOS 15+ (Foundation surface only; image encoding is #if canImport(UIKit)).

Installation

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/Luminoid/Sophon.git", from: "0.2.0"),
]

Sophon is listed on the Swift Package Index, which hosts the DocC API reference for SophonCore and SophonGemini.

Usage

Each app defines one configuration and one shared client:

import SophonGemini

extension GeminiClientConfiguration {
    static let myApp = GeminiClientConfiguration(
        keychainAccount: "com.myapp.geminiAPIKey",
        defaultModel: .gemini38Flash,
        availableModels: [.gemini31FlashLite, .gemini31Pro, .gemini35FlashLite, .gemini38Flash],
        retryPolicy: .default,
        logHandler: { level, message in MyLogger.log(level, message) }
    )
}

extension GeminiAPIClient {
    static let shared = GeminiAPIClient(configuration: .myApp)
}

One-call structured generation (the common case):

struct Extraction: Decodable { let title: String }

let result = try await GeminiAPIClient.shared.generateStructured(
    Extraction.self,
    label: "extract",
    prompt: promptText,
    schema: .object(properties: ["title": .string()], required: ["title"])
)

Multi-turn plain text:

let reply = try await GeminiAPIClient.shared.generateText(
    label: "followUp",
    contents: conversationContents
)

Image-heavy flows use the closure-based send so retries can re-encode smaller images and swap models:

let result = try await client.send(MyResult.self, label: "identify") { variant in
    let parts = try await client.encodeImages(images, variant: variant)
    return try client.buildRequest(parts: parts, promptText: prompt, apiKey: apiKey, modelID: variant.modelID, responseSchema: schema)
}

Retry policies

Retry behavior is a parameter, not a baked-in default. Set it per app in the configuration, or override per call.

.default .minimal
Attempts 3 3
Backoff 0.8s base, 6s cap, deterministic jitter 1s base, fixed exponential
Retry-After header honored (clamped) ignored
404 (retired model) retries the call on the fallback model fails the call
Transport failure re-encodes images smaller no re-encode

Either way, a 404 persists a reset of the stored model selection to fallbackModel, so the user's next call succeeds.

Model catalog

GeminiModel carries the full preset catalog; availableModels scopes what an app offers. A stored selection outside the app's catalog walks GeminiModel.successor (Google's documented replacements) and only then falls back, so pruning presets never strands a user's stored choice. .custom(id) always passes through.

Pick defaultModel and fallbackModel from models with a Gemini API free tier: end users supply their own API key, and the out-of-box experience should work on a free key with no billing enabled. Paid-only models (such as Gemini 3.1 Pro) fit availableModels as an explicit opt-in.

Error copy

GeminiError descriptions resolve from the package's string catalog (en, es, zh-Hans, zh-Hant) with app-neutral wording. Apps that want feature-specific copy ("Gemini returned a trip we couldn't read") map the cases at their feature layer.

Example App

The Example/ directory contains a small catalog app exercising the package end to end: API key and model settings, schema-constrained structured output, multi-turn chat, and the offline JSON extractor (no API key needed). It uses XcodeGen to generate the Xcode project:

cd Example
xcodegen generate
open SophonExample.xcodeproj

Development

brew bundle      # install swiftlint + swiftformat + xcodegen
make setup-hooks # wire pre-commit lint + format
make check       # SwiftLint --strict + SwiftFormat --lint
make test        # xcodebuild, iOS simulator (canonical)
make test-host   # swift test (fast, Foundation-only surface)

Used in

Sophon carries the Gemini integration in three App Store apps for iPhone, iPad, and Mac:

App What it is
Plantfolio Plant care: AI plant identification, seasonal watering schedules, collections (site)
Petfolio Pet care: health logs, vet visits, medication schedules, Family Sharing (site)
TripDays Collaborative travel planner: itineraries, paste-to-fill travel links, shared trips, expense splitting (site)

License

MIT. © Luminoid. See LICENSE and CHANGELOG.

Related projects

  • Monolith: CLI that scaffolds iOS apps, Swift Packages, and Swift CLIs (Sophon was scaffolded with it)
  • Everything else at luminoid.dev

About

Gemini AI client kit for Swift (iOS, macOS, Mac Catalyst): structured output, retry policies, model catalog with automatic fallback, lenient LLM JSON decoding. Ships in three App Store apps.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages