{"id":30354533,"url":"https://github.com/ricky-stone/swiftfm","last_synced_at":"2026-04-10T08:12:37.563Z","repository":{"id":310144511,"uuid":"1038875802","full_name":"ricky-stone/SwiftFM","owner":"ricky-stone","description":"Super-simple API for working with Foundation Models in Swift","archived":false,"fork":false,"pushed_at":"2026-02-14T02:17:19.000Z","size":59,"stargazers_count":11,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-02-14T08:59:36.152Z","etag":null,"topics":["ai","apple-intelligence","foundationmodels","generative-ai","ios","language-model","machine-learning","macos","swift","swift6","swiftpm"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ricky-stone.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":"rickystone","tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":null,"thanks_dev":null,"custom":null}},"created_at":"2025-08-16T01:48:35.000Z","updated_at":"2026-02-14T02:17:21.000Z","dependencies_parsed_at":"2025-08-16T03:48:24.870Z","dependency_job_id":"c12f0634-d60b-4ac8-a202-687cb38b6e56","html_url":"https://github.com/ricky-stone/SwiftFM","commit_stats":null,"previous_names":["ricky-stone/swiftfm"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/ricky-stone/SwiftFM","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ricky-stone%2FSwiftFM","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ricky-stone%2FSwiftFM/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ricky-stone%2FSwiftFM/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ricky-stone%2FSwiftFM/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ricky-stone","download_url":"https://codeload.github.com/ricky-stone/SwiftFM/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ricky-stone%2FSwiftFM/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31634231,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-10T07:40:12.752Z","status":"ssl_error","status_checked_at":"2026-04-10T07:40:11.664Z","response_time":98,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","apple-intelligence","foundationmodels","generative-ai","ios","language-model","machine-learning","macos","swift","swift6","swiftpm"],"created_at":"2025-08-19T03:33:51.052Z","updated_at":"2026-04-10T08:12:37.556Z","avatar_url":"https://github.com/ricky-stone.png","language":"Swift","funding_links":["https://ko-fi.com/rickystone"],"categories":[],"sub_categories":[],"readme":"# SwiftFM\n\n[![Swift](https://img.shields.io/badge/Swift-6.2%2B-F05138?logo=swift\u0026logoColor=white)](https://www.swift.org)\n[![Platforms](https://img.shields.io/badge/Platforms-iOS%2026%2B%20%7C%20macOS%2026%2B%20%7C%20visionOS%2026%2B-0A84FF)](https://developer.apple.com/documentation/foundationmodels)\n[![Release](https://img.shields.io/github/v/release/ricky-stone/SwiftFM?sort=semver)](https://github.com/ricky-stone/SwiftFM/releases)\n[![License](https://img.shields.io/github/license/ricky-stone/SwiftFM)](https://github.com/ricky-stone/SwiftFM/blob/main/LICENSE)\n[![Discussions](https://img.shields.io/github/discussions/ricky-stone/SwiftFM)](https://github.com/ricky-stone/SwiftFM/discussions)\n[![Stars](https://img.shields.io/github/stars/ricky-stone/SwiftFM?style=social)](https://github.com/ricky-stone/SwiftFM/stargazers)\n\nSwiftFM is a beginner-first Swift wrapper around Apple Foundation Models.\n\nVersion `2.0.0` keeps the original power-user features, but makes the package feel much more like SwiftUI:\n\n- modifier-style config chains\n- modifier-style request chains\n- modifier-style prompt chains\n- dynamic schemas and structured streaming\n- locale helpers, token counting, and feedback attachment export\n- custom adapter helpers\n\nIf you already use the older `Config(...)` and `RequestConfig(...)` style, it still works.\n\n## Requirements\n\n- Swift `6.2+`\n- Xcode `26+`\n- iOS `26+`\n- macOS `26+`\n- visionOS `26+`\n- Apple Intelligence enabled on supported hardware\n\n## Installation\n\nAdd the package with Swift Package Manager:\n\n```swift\n.package(url: \"https://github.com/ricky-stone/SwiftFM.git\", from: \"2.0.0\")\n```\n\n## 30-Second Start\n\n```swift\nimport SwiftFM\n\nlet fm = SwiftFM()\nlet text = try await fm.generateText(\n    for: \"Explain a snooker century break in one sentence.\"\n)\n\nprint(text)\n```\n\n## Beginner Style\n\nThis is the new `2.0` feel.\n\nYou start from `SwiftFM.configuration()`, `SwiftFM.request()`, or `SwiftFM.prompt(...)`, then chain small modifiers.\n\n```swift\nimport SwiftFM\n\nlet fm = SwiftFM(\n    config: SwiftFM.configuration()\n        .system(\"You are clear, friendly, and concise.\")\n        .model(.general)\n        .temperature(0.3)\n        .maximumResponseTokens(180)\n        .postProcessing(.readableParagraphs)\n)\n\nlet text = try await fm.generateText(\n    for: \"Write a short beginner explanation of snooker safety play.\"\n)\n```\n\n## One-Off Request Overrides\n\nUse `SwiftFM.request()` when you only want to change one call.\n\n```swift\nlet text = try await fm.generateText(\n    for: \"Write a short match preview.\",\n    request: SwiftFM.request()\n        .temperature(0.2)\n        .maximumResponseTokens(120)\n        .postProcessing(.readableParagraphs)\n)\n```\n\n## Output Cleanup\n\n`TextPostProcessing` is still here, and now it chains nicely too.\n\n```swift\nlet fm = SwiftFM(\n    config: SwiftFM.configuration()\n        .postProcessing(\n            .none\n                .trimmingWhitespace()\n                .collapsingSpacesAndTabs()\n                .limitingConsecutiveNewlines(to: 2)\n                .roundingFloatingPointNumbers(to: 0)\n        )\n)\n```\n\n## Prompt Builder\n\n`PromptSpec` now chains cleanly too.\n\n```swift\nlet spec = SwiftFM.prompt(\"Write a pre-match analysis.\")\n    .rule(\"Use plain text only\")\n    .rule(\"Do not use markdown\")\n    .requirement(\"Exactly 3 short paragraphs\")\n    .tone(\"Professional and engaging\")\n\nlet text = try await fm.generateText(from: spec)\nprint(text)\n```\n\n## Context Models\n\nIf your app already has Swift models, pass them directly.\n\n```swift\nstruct MatchVision: Codable, Sendable {\n    let home: String\n    let away: String\n    let venue: String\n    let bestOfFrames: Int\n}\n\nlet vision = MatchVision(\n    home: \"Judd Trump\",\n    away: \"Mark Allen\",\n    venue: \"Alexandra Palace\",\n    bestOfFrames: 11\n)\n\nlet summary = try await fm.generateText(\n    for: \"Write a short neutral preview using only this data.\",\n    context: vision,\n    request: SwiftFM.request()\n        .postProcessing(.readableParagraphs)\n)\n```\n\n### Context Formatting\n\nYou can still control how the JSON is embedded in the prompt.\n\n```swift\nlet text = try await fm.generateText(\n    for: \"Summarize this payload for a beginner.\",\n    context: vision,\n    request: SwiftFM.request()\n        .contextOptions(\n            .init()\n                .heading(\"Match Payload\")\n                .jsonFormatting(.compactSorted)\n        )\n)\n```\n\n## Text Streaming\n\nSwiftFM still supports both full snapshots and delta chunks.\n\n### Snapshot stream\n\n```swift\nfor try await snapshot in await fm.streamText(\n    for: \"Explain snooker break-building in three short paragraphs.\"\n) {\n    print(snapshot)\n}\n```\n\n### Delta stream\n\n```swift\nvar text = \"\"\n\nfor try await delta in await fm.streamTextDeltas(\n    for: \"Explain snooker break-building in three short paragraphs.\"\n) {\n    text += delta\n}\n```\n\n## SwiftUI Example\n\n```swift\nimport SwiftUI\nimport SwiftFM\n\nstruct HomeView: View {\n    @State private var text = \"\"\n    @State private var isLoading = true\n\n    private let fm = SwiftFM(\n        config: .beginnerFriendly\n            .system(\"You explain things simply.\")\n            .temperature(0.3)\n    )\n\n    var body: some View {\n        ZStack {\n            ScrollView {\n                Text(text)\n                    .frame(maxWidth: .infinity, alignment: .leading)\n                    .padding()\n            }\n\n            if isLoading {\n                ProgressView(\"Thinking...\")\n            }\n        }\n        .task {\n            do {\n                for try await delta in await fm.streamTextDeltas(\n                    from: SwiftFM.prompt(\"Explain one snooker safety drill.\")\n                        .requirement(\"Exactly 2 short paragraphs\")\n                ) {\n                    if isLoading { isLoading = false }\n                    text += delta\n                }\n            } catch {\n                isLoading = false\n                text = \"Error: \\(error.localizedDescription)\"\n            }\n        }\n    }\n}\n```\n\n## Typed Output with `@Generable`\n\n```swift\nimport SwiftFM\nimport FoundationModels\n\n@Generable\nstruct MatchPrediction: Decodable, Sendable {\n    @Guide(description: \"Home player\")\n    let home: String\n\n    @Guide(description: \"Away player\")\n    let away: String\n\n    @Guide(description: \"Predicted winner\")\n    let winner: String\n\n    @Guide(description: \"Confidence from 0.0 to 1.0\")\n    let confidence: Double\n}\n\nlet prediction = try await fm.generateJSON(\n    for: \"Predict this match and return home, away, winner, and confidence.\",\n    as: MatchPrediction.self\n)\n```\n\n### Structured Streaming\n\nNew in `2.0`: you can stream partial typed snapshots, not just text.\n\n```swift\nfor try await partial in await fm.streamJSON(\n    for: \"Generate a snooker match prediction.\",\n    as: MatchPrediction.self\n) {\n    print(partial.winner ?? \"Waiting...\")\n}\n```\n\n### Apple `26.4` Nil Handling\n\nIf you want to use Apple's newer explicit-nil generation behavior, you can do that directly with Foundation Models and still use SwiftFM normally:\n\n```swift\n@Generable(representNilExplicitlyInGeneratedContent: true)\nstruct OptionalNote: Decodable, Sendable {\n    let title: String\n    let subtitle: String?\n}\n```\n\n## Dynamic Schemas\n\nNew in `2.0`: you can generate runtime-structured content without creating a Swift type first.\n\n```swift\nimport FoundationModels\n\nlet schema = DynamicGenerationSchema(\n    name: \"SnookerNote\",\n    properties: [\n        .init(\n            name: \"title\",\n            description: \"Short title\",\n            schema: .init(type: String.self)\n        ),\n        .init(\n            name: \"frameCount\",\n            description: \"Likely number of frames\",\n            schema: .init(type: Int.self, guides: [.range(1 ... 35)])\n        )\n    ]\n)\n\nlet content = try await fm.generateContent(\n    for: \"Generate a snooker match note with a title and likely frame count.\",\n    dynamicSchema: schema\n)\n\nlet title = try content.value(String.self, forProperty: \"title\")\nlet frames = try content.value(Int.self, forProperty: \"frameCount\")\n```\n\n### Dynamic Schema Streaming\n\n```swift\nfor try await snapshot in await fm.streamContent(\n    for: \"Generate a short structured match note.\",\n    dynamicSchema: schema\n) {\n    print(snapshot.jsonString)\n}\n```\n\n## Tool Calling\n\nUse tools when the model should fetch live data or call app logic.\n\n```swift\nimport SwiftFM\nimport FoundationModels\n\n@Generable\nstruct MatchLookupArgs: Decodable, Sendable {\n    @Guide(description: \"Match id to fetch\")\n    let id: String\n}\n\nstruct MatchLookupTool: Tool {\n    let name = \"match_lookup\"\n    let description = \"Fetches match JSON by id\"\n\n    func call(arguments: MatchLookupArgs) async throws -\u003e String {\n        \"\"\"\n        {\"id\":\"\\(arguments.id)\",\"home\":\"Player A\",\"away\":\"Player B\",\"venue\":\"Main Arena\"}\n        \"\"\"\n    }\n}\n\nlet text = try await fm.generateText(\n    for: \"Use match_lookup for id 123, then write a short neutral preview.\",\n    request: SwiftFM.request()\n        .tool(MatchLookupTool())\n)\n```\n\n## Sampling and Temperature\n\nIf you want more control, the existing sampling features are still available.\n\n```swift\nlet fm = SwiftFM(\n    config: SwiftFM.configuration()\n        .model(.general)\n        .temperature(0.2)\n        .maximumResponseTokens(250)\n        .sampling(.greedy)\n)\n```\n\n## Model Selection\n\nSwiftFM model options:\n\n- `.default`\n- `.general`\n- `.contentTagging`\n- `.custom(SystemLanguageModel)`\n\n```swift\nlet summary = try await fm.generateText(\n    for: \"Give one tactical snooker tip.\",\n    using: .general\n)\n\nlet label = try await fm.generateText(\n    for: \"Return one label only: billing, support, bug. Text: app crashes at launch.\",\n    using: .contentTagging\n)\n```\n\n## Custom `SystemLanguageModel`\n\nIf you want the raw Apple surface, that is still supported too.\n\n```swift\nimport FoundationModels\n\nlet customModel = SystemLanguageModel(\n    useCase: .general,\n    guardrails: .default\n)\n\nlet fm = SwiftFM(\n    config: SwiftFM.configuration()\n        .model(.custom(customModel))\n)\n```\n\n## Custom Adapters\n\nNew in `2.0`: adapter helpers make Apple adapter usage easier to discover.\n\n```swift\nlet fm = SwiftFM(\n    config: .beginnerFriendly\n        .model(try .adapter(named: \"MyAdapter\"))\n)\n```\n\nYou can also load an adapter from disk:\n\n```swift\nlet model = try SwiftFM.Model.adapter(fileURL: adapterURL)\nlet fm = SwiftFM(config: .init(model: model))\n```\n\n## Availability, Languages, and Locale\n\n```swift\nif SwiftFM.isModelAvailable \u0026\u0026 SwiftFM.supportsCurrentLocale() {\n    print(\"Ready\")\n} else {\n    print(\"Unavailable: \\(SwiftFM.modelAvailability)\")\n}\n\nlet languages = SwiftFM.supportedLanguages(for: .default)\nprint(languages)\n```\n\n## Token Counting (`26.4+`)\n\nApple added token counting in `26.4`, and SwiftFM now exposes it.\n\n```swift\nif #available(iOS 26.4, macOS 26.4, visionOS 26.4, *) {\n    let count = try await fm.tokenCount(\n        from: SwiftFM.prompt(\"Explain a snooker safety shot.\")\n            .requirement(\"One sentence only\")\n    )\n\n    print(\"Prompt tokens:\", count)\n}\n```\n\nThere are also static helpers for tools, schemas, and transcript entries:\n\n```swift\nif #available(iOS 26.4, macOS 26.4, visionOS 26.4, *) {\n    let count = try await SwiftFM.tokenCount(for: schema)\n    print(count)\n}\n```\n\n## Feedback Attachments\n\nApple recommends exporting feedback attachments when a response is poor or guardrails trigger unexpectedly.\n\nNew in `2.0`: you can export that attachment directly from the current session.\n\n```swift\nlet attachment = await fm.feedbackAttachment(\n    sentiment: .negative,\n    issues: [\n        .init(category: .didNotFollowInstructions, explanation: \"It ignored the output format.\")\n    ],\n    desiredResponseText: \"A short plain-text answer in exactly two sentences.\"\n)\n\nprint(\"Attachment bytes:\", attachment.count)\n```\n\n## Session Helpers\n\n```swift\nlet fm = SwiftFM(\n    config: .beginnerFriendly\n        .system(\"You are concise.\")\n)\n\nawait fm.prewarm(promptPrefix: \"Match analysis\")\nlet busy = await fm.isBusy\nlet transcript = await fm.transcript\nawait fm.resetConversation()\n```\n\nWhat these do:\n\n- `prewarm(promptPrefix:)`: reduce first-response latency\n- `isBusy`: `true` while the session is generating\n- `transcript`: inspect the current conversation history\n- `resetConversation()`: clear the session and start fresh with the same base config\n\n## Error Handling\n\n```swift\ndo {\n    let text = try await fm.generateText(for: \"Analyze this match.\")\n    print(text)\n} catch let error as SwiftFM.SwiftFMError {\n    print(error.localizedDescription)\n\n    if let generationError = error.generationError {\n        print(\"Foundation Models error:\", generationError)\n    }\n} catch {\n    print(error.localizedDescription)\n}\n```\n\n## Existing APIs Still Work\n\n`2.0.0` adds fluent builder-style usage, but it does not remove the current feature set.\n\nThese still work:\n\n- `SwiftFM(config: .init(...))`\n- `RequestConfig(...)`\n- `PromptSpec(...)`\n- `generateText`\n- `streamText`\n- `streamTextDeltas`\n- `generateJSON`\n- request-scoped tools\n- context embedding options\n- post-processing options\n- custom `SystemLanguageModel`\n\n## Version\n\n- Current source version: `2.0.0`\n\n## License\n\nSwiftFM is licensed under the MIT License. See `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fricky-stone%2Fswiftfm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fricky-stone%2Fswiftfm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fricky-stone%2Fswiftfm/lists"}