├── .gitignore ├── Package@swift-5.swift ├── .github └── workflows │ └── ci.yml ├── Sources └── Ollama │ ├── Embeddings.swift │ ├── Extensions │ ├── JSONDecoder+Extensions.swift │ └── Data+Extensions.swift │ ├── KeepAlive.swift │ ├── Tool.swift │ ├── Chat.swift │ ├── Model.swift │ ├── Value.swift │ └── Client.swift ├── Package.swift ├── LICENSE.md ├── Tests └── OllamaTests │ ├── Fixtures │ └── HexColorTool.swift │ ├── ISO8601WithFractionalSecondsTests.swift │ ├── DataURLTests.swift │ ├── ValueTests.swift │ ├── ToolTests.swift │ ├── ModelTests.swift │ ├── KeepAliveTests.swift │ └── ClientTests.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Package@swift-5.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Ollama", 8 | platforms: [ 9 | .macOS(.v13), 10 | .macCatalyst(.v13), 11 | .iOS(.v16), 12 | .watchOS(.v9), 13 | .tvOS(.v16) 14 | ], 15 | products: [ 16 | .library( 17 | name: "Ollama", 18 | targets: ["Ollama"]) 19 | ], 20 | targets: [ 21 | .target( 22 | name: "Ollama") 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | test: 11 | runs-on: macos-latest 12 | 13 | strategy: 14 | matrix: 15 | swift-version: 16 | - ^5 17 | - ^6 18 | 19 | name: Build and Test (Swift ${{ matrix.swift-version }}) 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: swift-actions/setup-swift@v2 24 | with: 25 | swift-version: ${{ matrix.swift-version }} 26 | - name: Build 27 | run: swift build -v 28 | - name: Run tests 29 | if: ${{ matrix.swift-version == '^6' }} 30 | run: swift test -v 31 | -------------------------------------------------------------------------------- /Sources/Ollama/Embeddings.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Embeddings: RawRepresentable, Hashable, Sendable { 4 | public let rawValue: [[Double]] 5 | 6 | public init(rawValue: [[Double]]) { 7 | self.rawValue = rawValue 8 | } 9 | } 10 | 11 | // MARK: - ExpressibleByArrayLiteral 12 | 13 | extension Embeddings: ExpressibleByArrayLiteral { 14 | public init(arrayLiteral elements: [Double]...) { 15 | self.init(rawValue: elements) 16 | } 17 | } 18 | 19 | // MARK: - Decodable 20 | 21 | extension Embeddings: Decodable { 22 | public init(from decoder: Decoder) throws { 23 | let container = try decoder.singleValueContainer() 24 | rawValue = try container.decode([[Double]].self) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import CompilerPluginSupport 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "Ollama", 9 | platforms: [ 10 | .macOS(.v13), 11 | .macCatalyst(.v13), 12 | .iOS(.v16), 13 | .watchOS(.v9), 14 | .tvOS(.v16), 15 | .visionOS(.v1), 16 | ], 17 | products: [ 18 | .library( 19 | name: "Ollama", 20 | targets: ["Ollama"]) 21 | ], 22 | targets: [ 23 | .target( 24 | name: "Ollama", 25 | dependencies: []), 26 | .testTarget( 27 | name: "OllamaTests", 28 | dependencies: ["Ollama"]), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 Mattt (https://mat.tt) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Sources/Ollama/Extensions/JSONDecoder+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension JSONDecoder.DateDecodingStrategy { 4 | /// A custom date decoding strategy that handles ISO8601 formatted dates with optional fractional seconds. 5 | /// 6 | /// This strategy attempts to parse dates in the following order: 7 | /// 1. ISO8601 with fractional seconds 8 | /// 2. ISO8601 without fractional seconds 9 | /// 10 | /// If both parsing attempts fail, it throws a `DecodingError.dataCorruptedError`. 11 | /// 12 | /// - Returns: A `DateDecodingStrategy` that can be used with a `JSONDecoder`. 13 | public static let iso8601WithFractionalSeconds = custom { decoder in 14 | let container = try decoder.singleValueContainer() 15 | let string = try container.decode(String.self) 16 | 17 | let formatter = ISO8601DateFormatter() 18 | formatter.formatOptions = [ 19 | .withInternetDateTime, 20 | .withFractionalSeconds, 21 | ] 22 | 23 | if let date = formatter.date(from: string) { 24 | return date 25 | } 26 | 27 | // Try again without fractional seconds 28 | formatter.formatOptions = [.withInternetDateTime] 29 | 30 | guard let date = formatter.date(from: string) else { 31 | throw DecodingError.dataCorruptedError( 32 | in: container, debugDescription: "Invalid date: \(string)") 33 | } 34 | 35 | return date 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/OllamaTests/Fixtures/HexColorTool.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Ollama 3 | 4 | struct HexColorInput: Codable { 5 | let red: Double 6 | let green: Double 7 | let blue: Double 8 | } 9 | 10 | let hexColorTool = Tool( 11 | name: "rgb_to_hex", 12 | description: """ 13 | Converts RGB components to a hexadecimal color string. 14 | 15 | The input is a JSON object with three floating-point numbers 16 | representing the red, green, and blue components of a color. 17 | The output is a string representing the color in hexadecimal format. 18 | 19 | Parameters are named red, green, and blue. 20 | Values are floating-point numbers between 0.0 and 1.0. 21 | """, 22 | parameters: [ 23 | "red": [ 24 | "type": "number", 25 | "description": "The red component of the color", 26 | "minimum": 0.0, 27 | "maximum": 1.0, 28 | ], 29 | "green": [ 30 | "type": "number", 31 | "description": "The green component of the color", 32 | "minimum": 0.0, 33 | "maximum": 1.0, 34 | ], 35 | "blue": [ 36 | "type": "number", 37 | "description": "The blue component of the color", 38 | "minimum": 0.0, 39 | "maximum": 1.0, 40 | ], 41 | ], 42 | required: ["red", "green", "blue"] 43 | ) { (input) async throws -> String in 44 | let r = Int(round(input.red * 255)) 45 | let g = Int(round(input.green * 255)) 46 | let b = Int(round(input.blue * 255)) 47 | return String( 48 | format: "#%02X%02X%02X", 49 | min(max(r, 0), 255), 50 | min(max(g, 0), 255), 51 | min(max(b, 0), 255) 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /Tests/OllamaTests/ISO8601WithFractionalSecondsTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | 4 | struct ISO8601WithFractionalSecondsTests { 5 | struct TestContainer: Codable { 6 | let date: Date 7 | } 8 | 9 | let calendar: Calendar 10 | let decoder: JSONDecoder 11 | let encoder: JSONEncoder 12 | 13 | init() { 14 | var utc = Calendar(identifier: .gregorian) 15 | utc.timeZone = TimeZone(identifier: "UTC")! 16 | calendar = utc 17 | 18 | decoder = JSONDecoder() 19 | decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds 20 | 21 | encoder = JSONEncoder() 22 | encoder.dateEncodingStrategy = .iso8601 23 | } 24 | 25 | @Test 26 | func testDecodingISO8601WithFractionalSeconds() throws { 27 | let json = #"{"date": "2023-01-01T12:34:56.789Z"}"# 28 | let container = try decoder.decode(TestContainer.self, from: json.data(using: .utf8)!) 29 | 30 | let components = calendar.dateComponents( 31 | [.year, .month, .day, .hour, .minute, .second, .nanosecond], from: container.date) 32 | 33 | #expect(components.year == 2023) 34 | #expect(components.month == 1) 35 | #expect(components.day == 1) 36 | #expect(components.hour == 12) 37 | #expect(components.minute == 34) 38 | #expect(components.second == 56) 39 | #expect(abs((components.nanosecond ?? 0) - 789_000_000) < 1_000) 40 | } 41 | 42 | @Test 43 | func testDecodingISO8601WithoutFractionalSeconds() throws { 44 | let json = #"{"date": "2023-04-15T12:30:45Z"}"# 45 | let container = try decoder.decode(TestContainer.self, from: json.data(using: .utf8)!) 46 | 47 | let components = calendar.dateComponents( 48 | [.year, .month, .day, .hour, .minute, .second, .nanosecond], from: container.date) 49 | 50 | #expect(components.year == 2023) 51 | #expect(components.month == 4) 52 | #expect(components.day == 15) 53 | #expect(components.hour == 12) 54 | #expect(components.minute == 30) 55 | #expect(components.second == 45) 56 | #expect(components.nanosecond == 0) 57 | } 58 | 59 | @Test 60 | func testDecodeInvalidDate() throws { 61 | let json = #""invalid""# 62 | 63 | do { 64 | _ = try decoder.decode(Date.self, from: json.data(using: .utf8)!) 65 | Issue.record("Expected DecodingError.dataCorrupted") 66 | } catch DecodingError.dataCorrupted(let context) { 67 | #expect(context.debugDescription == "Invalid date: invalid") 68 | } catch { 69 | Issue.record("Expected DecodingError.dataCorrupted, got \(error)") 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/OllamaTests/DataURLTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import Ollama 4 | 5 | struct DataExtensionsTests { 6 | @Test 7 | func testIsDataURL() { 8 | #expect(Data.isDataURL(string: "data:,A%20brief%20note")) 9 | #expect(Data.isDataURL(string: "data:text/plain,Hello%2C%20World%21")) 10 | #expect(Data.isDataURL(string: "data:text/plain;charset=utf-8,Hello%2C%20World%21")) 11 | #expect(Data.isDataURL(string: "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==")) 12 | #expect(!Data.isDataURL(string: "https://example.com")) 13 | } 14 | 15 | @Test 16 | func testParseDataURLPlainText() { 17 | let url = "data:,A%20brief%20note" 18 | let result = Data.parseDataURL(url) 19 | #expect(result != nil) 20 | #expect(result?.mimeType == "text/plain") 21 | #expect(result?.data == "A brief note".data(using: .utf8)) 22 | } 23 | 24 | @Test 25 | func testParseDataURLWithMimeType() { 26 | let url = "data:text/plain,Hello%2C%20World%21" 27 | let result = Data.parseDataURL(url) 28 | #expect(result != nil) 29 | #expect(result?.mimeType == "text/plain") 30 | #expect(result?.data == "Hello, World!".data(using: .utf8)) 31 | } 32 | 33 | @Test 34 | func testParseDataURLWithCharset() { 35 | let url = "data:text/plain;charset=utf-8,Hello%2C%20World%21" 36 | let result = Data.parseDataURL(url) 37 | #expect(result != nil) 38 | #expect(result?.mimeType == "text/plain;charset=utf-8") 39 | #expect(result?.data == "Hello, World!".data(using: .utf8)) 40 | } 41 | 42 | @Test 43 | func testParseDataURLBase64Encoded() { 44 | let url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==" 45 | let result = Data.parseDataURL(url) 46 | #expect(result != nil) 47 | #expect(result?.mimeType == "text/plain") 48 | #expect(result?.data == "Hello, World!".data(using: .utf8)) 49 | } 50 | 51 | @Test 52 | func testParseDataURLInvalid() { 53 | let url = "invalid" 54 | let result = Data.parseDataURL(url) 55 | #expect(result == nil) 56 | } 57 | 58 | @Test 59 | func testParseDataURLImageGifBase64() { 60 | let url = 61 | "data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAwAAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFzByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSpa/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJlZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uisF81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PHhhx4dbgYKAAA7" 62 | let result = Data.parseDataURL(url) 63 | #expect(result != nil) 64 | #expect(result?.mimeType == "image/gif") 65 | #expect(result?.data.prefix(6) == Data([0x47, 0x49, 0x46, 0x38, 0x37, 0x61])) // "GIF87a" 66 | } 67 | 68 | @Test 69 | func testDataURLEncoded() { 70 | let testData = "Hello, World!".data(using: .utf8)! 71 | let encoded = testData.dataURLEncoded(mimeType: "text/plain") 72 | #expect(encoded == "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==") 73 | } 74 | 75 | @Test 76 | func testDataURLEncodedDefaultMimeType() { 77 | let testData = "Hello, World!".data(using: .utf8)! 78 | let encoded = testData.dataURLEncoded() 79 | #expect(encoded == "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Ollama/Extensions/Data+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RegexBuilder 3 | 4 | extension Data { 5 | /// Regex pattern for data URLs 6 | @inline(__always) private static var dataURLRegex: 7 | Regex<(Substring, Substring, Substring?, Substring)> 8 | { 9 | Regex { 10 | "data:" 11 | Capture { 12 | ZeroOrMore(.reluctant) { 13 | CharacterClass.anyOf(",;").inverted 14 | } 15 | } 16 | Optionally { 17 | ";charset=" 18 | Capture { 19 | OneOrMore(.reluctant) { 20 | CharacterClass.anyOf(",;").inverted 21 | } 22 | } 23 | } 24 | Optionally { ";base64" } 25 | "," 26 | Capture { 27 | ZeroOrMore { .any } 28 | } 29 | } 30 | } 31 | 32 | /// Checks if a given string is a valid data URL. 33 | /// 34 | /// - Parameter string: The string to check. 35 | /// - Returns: `true` if the string is a valid data URL, otherwise `false`. 36 | /// - SeeAlso: [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397.html) 37 | public static func isDataURL(string: String) -> Bool { 38 | return string.wholeMatch(of: dataURLRegex) != nil 39 | } 40 | 41 | /// Parses a data URL string into its MIME type and data components. 42 | /// 43 | /// - Parameter string: The data URL string to parse. 44 | /// - Returns: A tuple containing the MIME type and decoded data, or `nil` if parsing fails. 45 | /// - SeeAlso: [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397.html) 46 | public static func parseDataURL(_ string: String) -> (mimeType: String, data: Data)? { 47 | guard let match = string.wholeMatch(of: dataURLRegex) else { 48 | return nil 49 | } 50 | 51 | // Extract components using strongly typed captures 52 | let (_, mediatype, charset, encodedData) = match.output 53 | 54 | let isBase64 = string.contains(";base64,") 55 | 56 | // Process MIME type 57 | var mimeType = mediatype.isEmpty ? "text/plain" : String(mediatype) 58 | if let charset = charset, !charset.isEmpty, mimeType.starts(with: "text/") { 59 | mimeType += ";charset=\(charset)" 60 | } 61 | 62 | // Decode data 63 | let decodedData: Data 64 | if isBase64 { 65 | guard let base64Data = Data(base64Encoded: String(encodedData)) else { return nil } 66 | decodedData = base64Data 67 | } else { 68 | guard 69 | let percentDecodedData = String(encodedData).removingPercentEncoding?.data( 70 | using: .utf8) 71 | else { return nil } 72 | decodedData = percentDecodedData 73 | } 74 | 75 | return (mimeType: mimeType, data: decodedData) 76 | } 77 | 78 | /// Encodes the data as a data URL string with an optional MIME type. 79 | /// 80 | /// - Parameter mimeType: The MIME type of the data. If `nil`, "text/plain" will be used. 81 | /// - Returns: A data URL string representation of the data. 82 | /// - SeeAlso: [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397.html) 83 | public func dataURLEncoded(mimeType: String? = nil) -> String { 84 | let base64Data = self.base64EncodedString() 85 | return "data:\(mimeType ?? "text/plain");base64,\(base64Data)" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/Ollama/KeepAlive.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Controls how long a model will stay loaded into memory following a request. 4 | public enum KeepAlive: Hashable, Sendable, Comparable { 5 | /// Use the server's default keep-alive behavior. 6 | case `default` 7 | 8 | /// Unload the model immediately after the request. 9 | case none 10 | 11 | /// Keep the model loaded for a specific duration. 12 | case duration(Duration) 13 | 14 | /// Keep the model loaded indefinitely. 15 | case forever 16 | 17 | /// Converts the KeepAlive enum to a Value for API requests. 18 | var value: Value? { 19 | switch self { 20 | case .default: 21 | return nil 22 | case .none: 23 | return .int(0) 24 | case .forever: 25 | return .int(-1) 26 | case .duration(let duration): 27 | switch duration.totalSeconds { 28 | case ..<0: 29 | return .int(-1) 30 | case 0: 31 | return .int(0) 32 | default: 33 | return .string(duration.description) 34 | } 35 | } 36 | } 37 | 38 | /// Duration specifications for keeping a model loaded. 39 | public enum Duration: Hashable, Sendable, Comparable { 40 | /// Keep loaded for the specified number of seconds. 41 | /// - Note: Zero values convert to `.none`, negative values convert to `.indefinite`. 42 | case seconds(Int) 43 | 44 | /// Keep loaded for the specified number of minutes. 45 | /// - Note: Zero values convert to `.none`, negative values convert to `.indefinite`. 46 | case minutes(Int) 47 | 48 | /// Keep loaded for the specified number of hours. 49 | /// - Note: Zero values convert to `.none`, negative values convert to `.indefinite`. 50 | case hours(Int) 51 | } 52 | } 53 | 54 | // MARK: - Convenience Initializers 55 | 56 | extension KeepAlive { 57 | /// Creates a KeepAlive value for the specified number of seconds. 58 | /// - Parameter seconds: The number of seconds to keep the model loaded. 59 | /// Zero converts to `.none`, negative values convert to `.indefinite`. 60 | /// - Returns: A KeepAlive value with the specified duration. 61 | public static func seconds(_ seconds: Int) -> KeepAlive { 62 | .duration(.seconds(seconds)) 63 | } 64 | 65 | /// Creates a KeepAlive value for the specified number of minutes. 66 | /// - Parameter minutes: The number of minutes to keep the model loaded. 67 | /// Zero converts to `.none`, negative values convert to `.indefinite`. 68 | /// - Returns: A KeepAlive value with the specified duration. 69 | public static func minutes(_ minutes: Int) -> KeepAlive { 70 | .duration(.minutes(minutes)) 71 | } 72 | 73 | /// Creates a KeepAlive value for the specified number of hours. 74 | /// - Parameter hours: The number of hours to keep the model loaded. 75 | /// Zero converts to `.none`, negative values convert to `.indefinite`. 76 | /// - Returns: A KeepAlive value with the specified duration. 77 | public static func hours(_ hours: Int) -> KeepAlive { 78 | .duration(.hours(hours)) 79 | } 80 | } 81 | 82 | // MARK: - Comparable 83 | 84 | extension KeepAlive { 85 | public static func < (lhs: KeepAlive, rhs: KeepAlive) -> Bool { 86 | switch (lhs, rhs) { 87 | // Equal cases always return false 88 | case (.default, .default): 89 | return false 90 | case (.none, .none): 91 | return false 92 | case (.forever, .forever): 93 | return false 94 | 95 | // Default is less than everything except itself 96 | case (.default, _): 97 | return true 98 | case (_, .default): 99 | return false 100 | 101 | // None is less than forever and duration, but greater than default 102 | case (.none, .forever), (.none, .duration): 103 | return true 104 | case (.none, _): 105 | return false 106 | 107 | // Forever is greater than everything except itself 108 | case (.forever, _): 109 | return false 110 | case (_, .forever): 111 | return true 112 | 113 | // Duration cases compare their total seconds 114 | case (.duration(let lhsDuration), .duration(let rhsDuration)): 115 | return lhsDuration < rhsDuration 116 | 117 | // Duration is greater than none 118 | case (.duration, .none): 119 | return false 120 | 121 | // Everything else is greater than none 122 | case (_, .none): 123 | return true 124 | } 125 | } 126 | } 127 | 128 | extension KeepAlive.Duration { 129 | public static func < (lhs: KeepAlive.Duration, rhs: KeepAlive.Duration) -> Bool { 130 | return lhs.totalSeconds < rhs.totalSeconds 131 | } 132 | 133 | /// Returns the total duration in seconds for comparison purposes. 134 | var totalSeconds: Int { 135 | switch self { 136 | case .seconds(let value): 137 | return value 138 | case .minutes(let value): 139 | return value * 60 140 | case .hours(let value): 141 | return value * 3600 142 | } 143 | } 144 | } 145 | 146 | // MARK: - CustomStringConvertible 147 | 148 | extension KeepAlive: CustomStringConvertible { 149 | public var description: String { 150 | switch self { 151 | case .default: 152 | return "default" 153 | case .none: 154 | return "none" 155 | case .forever: 156 | return "forever" 157 | case .duration(let duration): 158 | return duration.description 159 | } 160 | } 161 | } 162 | 163 | extension KeepAlive.Duration: CustomStringConvertible { 164 | public var description: String { 165 | switch self { 166 | case .seconds(let value): 167 | return "\(value)s" 168 | case .minutes(let value): 169 | return "\(value)m" 170 | case .hours(let value): 171 | return "\(value)h" 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Tests/OllamaTests/ValueTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | 3 | @testable import Ollama 4 | 5 | struct ValueTests { 6 | @Suite("Bool value conversions") 7 | struct BoolTests { 8 | @Test("Bool conversion in strict mode") 9 | func strict() { 10 | let cases = [ 11 | (Value.bool(true), true), 12 | (Value.bool(false), false), 13 | (Value.int(1), nil), 14 | (Value.string("true"), nil), 15 | ] 16 | 17 | for (value, expected) in cases { 18 | #expect(Bool(value) == expected) 19 | } 20 | } 21 | 22 | @Suite("Bool conversion in non-strict mode") 23 | struct NonStrict { 24 | @Test("Bool conversion from numbers") 25 | func numbers() { 26 | let cases = [ 27 | (Value.int(1), true), 28 | (Value.int(0), false), 29 | (Value.int(2), nil), 30 | (Value.double(1.0), true), 31 | (Value.double(0.0), false), 32 | (Value.double(0.5), nil), 33 | ] 34 | 35 | for (value, expected) in cases { 36 | #expect(Bool(value, strict: false) == expected) 37 | } 38 | } 39 | 40 | @Test("Bool conversion from strings") 41 | func strings() { 42 | let cases = [ 43 | // True cases 44 | (Value.string("true"), true), 45 | (Value.string("t"), true), 46 | (Value.string("yes"), true), 47 | (Value.string("y"), true), 48 | (Value.string("on"), true), 49 | (Value.string("1"), true), 50 | 51 | // False cases 52 | (Value.string("false"), false), 53 | (Value.string("f"), false), 54 | (Value.string("no"), false), 55 | (Value.string("n"), false), 56 | (Value.string("off"), false), 57 | (Value.string("0"), false), 58 | 59 | // Nil cases 60 | (Value.string("TRUE"), nil), 61 | (Value.string("YES"), nil), 62 | (Value.string("False"), nil), 63 | (Value.string("No"), nil), 64 | (Value.string("invalid"), nil), 65 | ] 66 | 67 | for (value, expected) in cases { 68 | #expect(Bool(value, strict: false) == expected) 69 | } 70 | } 71 | } 72 | } 73 | 74 | @Suite("Int value conversions") 75 | struct IntTests { 76 | @Test("Int conversion in strict mode") 77 | func strict() { 78 | let cases = [ 79 | (Value.int(42), 42), 80 | (Value.double(42.0), nil), 81 | (Value.string("42"), nil), 82 | ] 83 | 84 | for (value, expected) in cases { 85 | #expect(Int(value, strict: true) == expected) 86 | } 87 | } 88 | 89 | @Test("Int conversion in non-strict mode") 90 | func nonStrict() { 91 | let cases = [ 92 | (Value.double(42.0), 42), 93 | (Value.double(42.5), nil), 94 | (Value.string("42"), 42), 95 | (Value.string("42.5"), nil), 96 | (Value.string("invalid"), nil), 97 | ] 98 | 99 | for (value, expected) in cases { 100 | #expect(Int(value, strict: false) == expected) 101 | } 102 | } 103 | } 104 | 105 | @Suite("Double value conversions") 106 | struct DoubleTests { 107 | @Test("Double conversion in strict mode") 108 | func strict() { 109 | let cases = [ 110 | (Value.double(42.5), 42.5), 111 | (Value.int(42), 42.0), 112 | (Value.string("42.5"), nil), 113 | ] 114 | 115 | for (value, expected) in cases { 116 | #expect(Double(value, strict: true) == expected) 117 | } 118 | } 119 | 120 | @Test("Double conversion in non-strict mode") 121 | func nonStrict() { 122 | let cases = [ 123 | (Value.string("42.5"), 42.5), 124 | (Value.string("42"), 42.0), 125 | (Value.string("invalid"), nil), 126 | ] 127 | 128 | for (value, expected) in cases { 129 | #expect(Double(value, strict: false) == expected) 130 | } 131 | } 132 | } 133 | 134 | @Suite("String value conversions") 135 | struct StringTests { 136 | @Test("String conversion in strict mode") 137 | func strict() { 138 | let cases = [ 139 | (Value.string("hello"), "hello"), 140 | (Value.int(42), nil), 141 | (Value.double(42.5), nil), 142 | (Value.bool(true), nil), 143 | ] 144 | 145 | for (value, expected) in cases { 146 | #expect(String(value, strict: true) == expected) 147 | } 148 | } 149 | 150 | @Test("String conversion in non-strict mode") 151 | func nonStrict() { 152 | let cases = [ 153 | (Value.int(42), "42"), 154 | (Value.double(42.5), "42.5"), 155 | (Value.bool(true), "true"), 156 | (Value.bool(false), "false"), 157 | ] 158 | 159 | for (value, expected) in cases { 160 | #expect(String(value, strict: false) == expected) 161 | } 162 | } 163 | } 164 | 165 | @Test("Null value conversions") 166 | func nullValues() { 167 | #expect(Bool(Value.null) == nil) 168 | #expect(Int(Value.null) == nil) 169 | #expect(Double(Value.null) == nil) 170 | #expect(String(Value.null) == nil) 171 | } 172 | 173 | @Test("Array value conversions") 174 | func arrayValues() { 175 | let array = Value.array([.bool(true)]) 176 | #expect(Bool(array) == nil) 177 | #expect(Int(array) == nil) 178 | #expect(Double(array) == nil) 179 | #expect(String(array) == nil) 180 | } 181 | 182 | @Test("Object value conversions") 183 | func objectValues() { 184 | let object = Value.object(["key": .bool(true)]) 185 | #expect(Bool(object) == nil) 186 | #expect(Int(object) == nil) 187 | #expect(Double(object) == nil) 188 | #expect(String(object) == nil) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Sources/Ollama/Tool.swift: -------------------------------------------------------------------------------- 1 | /// Protocol defining the requirements for a tool that can be used with Ollama 2 | public protocol ToolProtocol: Sendable { 3 | /// The JSON Schema describing the tool's interface 4 | var schema: any (Codable & Sendable) { get } 5 | } 6 | 7 | /// A type representing a tool that can be used with Ollama. 8 | /// 9 | /// Tools allow models to perform complex tasks 10 | /// or interact with the outside world by calling functions, APIs, 11 | /// or other services. 12 | /// 13 | /// Tools can be provided to Ollama models that support tool calling 14 | /// (like Llama 3.1, Mistral Nemo, etc.) to extend their capabilities. 15 | public struct Tool: ToolProtocol { 16 | /// A JSON Schema for the tool. 17 | /// 18 | /// Models use the schema to understand when and how to use the tool. 19 | /// The schema includes the tool's name, description, and parameter specifications. 20 | /// 21 | /// - Example: 22 | /// ```swift 23 | /// var schema: [String: Value] { 24 | /// [ 25 | /// "type: "function", 26 | /// "function": [ 27 | /// "name": "get_current_weather", 28 | /// "description": "Get the current weather for a location", 29 | /// "parameters": [ 30 | /// "type": "object", 31 | /// "properties": [ 32 | /// "location": [ 33 | /// "type": "string", 34 | /// "description": "The location to get the weather for, e.g. San Francisco, CA" 35 | /// ], 36 | /// "format": [ 37 | /// "type": "string", 38 | /// "description": "The format to return the weather in, e.g. 'celsius' or 'fahrenheit'", 39 | /// "enum": ["celsius", "fahrenheit"] 40 | /// ] 41 | /// ], 42 | /// "required": ["location", "format"] 43 | /// ] 44 | /// ] 45 | /// ] 46 | /// } 47 | /// ``` 48 | public var schema: any (Codable & Sendable) { schemaValue } 49 | private(set) var schemaValue: Value 50 | 51 | /// The tool's implementation. 52 | /// 53 | /// This is the function that will be called when the tool is called. 54 | /// 55 | /// - Parameter input: The input parameters for the tool 56 | /// - Returns: The output of the tool operation 57 | /// - Throws: Any errors that occur during tool execution 58 | /// - SeeAlso: `callAsFunction(_:)` 59 | private let implementation: @Sendable (Input) async throws -> Output 60 | 61 | /// Creates a new tool with the given schema and implementation. 62 | /// 63 | /// - Parameters: 64 | /// - schema: The JSON schema describing the tool's interface 65 | /// - implementation: The function that implements the tool's behavior 66 | public init( 67 | schema: [String: Value], 68 | implementation: @Sendable @escaping (Input) async throws -> Output 69 | ) { 70 | self.schemaValue = Value.object([ 71 | "type": .string("function"), 72 | "function": .object(schema), 73 | ]) 74 | self.implementation = implementation 75 | } 76 | 77 | /// Creates a new tool with the given name, description, and implementation. 78 | /// 79 | /// - Parameters: 80 | /// - name: The name of the tool 81 | /// - description: A description of what the tool does 82 | /// - parameters: A JSON Schema for the tool's parameters 83 | /// - required: The required parameters of the tool. 84 | /// If not provided, all parameters are optional. 85 | /// - implementation: The function that implements the tool's behavior 86 | /// - Returns: A new Tool instance 87 | /// - Example: 88 | /// ```swift 89 | /// let weatherTool = Tool( 90 | /// name: "get_current_weather", 91 | /// description: "Get the current weather for a location", 92 | /// parameters: [ 93 | /// "type": "object", 94 | /// "properties": [ 95 | /// "location": [ 96 | /// "type": "string", 97 | /// "description": "The location to get the weather for" 98 | /// ] 99 | /// ], 100 | /// "required": ["location"] 101 | /// ] 102 | /// ) { (input: WeatherInput) async throws -> WeatherOutput in 103 | /// // Implementation here 104 | /// } 105 | /// ``` 106 | public init( 107 | name: String, 108 | description: String, 109 | parameters: [String: Value], 110 | required: [String] = [], 111 | implementation: @Sendable @escaping (Input) async throws -> Output 112 | ) { 113 | var propertiesObject: [String: Value] = parameters 114 | var requiredParams = required 115 | 116 | // Check if the user passed a full schema and extract properties and required fields 117 | if case .string("object") = parameters["type"], 118 | case .object(let props) = parameters["properties"] 119 | { 120 | 121 | #if DEBUG 122 | print( 123 | "Warning: You're passing a full JSON schema to the 'parameters' argument. " 124 | + "This usage is deprecated. Pass only the properties object instead.") 125 | #endif 126 | 127 | propertiesObject = props 128 | 129 | // If required field exists in the parameters and no required array was explicitly passed 130 | if required.isEmpty, 131 | case .array(let reqArray) = parameters["required"] 132 | { 133 | requiredParams = reqArray.compactMap { value in 134 | if case .string(let str) = value { 135 | return str 136 | } 137 | return nil 138 | } 139 | } 140 | } 141 | 142 | self.init( 143 | schema: [ 144 | "name": .string(name), 145 | "description": .string(description), 146 | "parameters": .object([ 147 | "type": .string("object"), 148 | "properties": .object(propertiesObject), 149 | "required": .array(requiredParams.map { .string($0) }), 150 | ]), 151 | ], 152 | implementation: implementation 153 | ) 154 | } 155 | 156 | /// Calls the tool with the given input. 157 | /// 158 | /// - Parameter input: The input parameters for the tool 159 | /// - Returns: The output of the tool operation 160 | /// - Throws: Any errors that occur during tool execution 161 | public func callAsFunction(_ input: Input) async throws -> Output { 162 | try await implementation(input) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Sources/Ollama/Chat.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Data 2 | import struct Foundation.Date 3 | import struct Foundation.TimeInterval 4 | 5 | /// Namespace for chat-related types and functionality. 6 | public enum Chat { 7 | /// Represents a message in a chat conversation. 8 | public struct Message: Hashable, Sendable { 9 | /// The role of the message sender. 10 | public enum Role: String, Hashable, CaseIterable, Codable, Sendable { 11 | /// A system message. 12 | case system 13 | 14 | /// A message from the user. 15 | case user 16 | 17 | /// A message from the AI assistant. 18 | case assistant 19 | 20 | /// The result of calling a tool. 21 | case tool 22 | } 23 | 24 | /// Represents a function call made by a tool 25 | public struct ToolCall: Hashable, Codable, Sendable { 26 | /// Represents the function details for a tool call 27 | public struct Function: Hashable, Codable, Sendable { 28 | /// The name of the function 29 | public let name: String 30 | 31 | /// The arguments passed to the function 32 | public let arguments: [String: Value] 33 | 34 | public init(name: String, arguments: [String: Value]) { 35 | self.name = name 36 | self.arguments = arguments 37 | } 38 | } 39 | 40 | /// The function to be called 41 | public let function: Function 42 | 43 | public init(function: Function) { 44 | self.function = function 45 | } 46 | } 47 | 48 | /// The role of the message sender. 49 | public let role: Role 50 | 51 | /// The content of the message. 52 | public let content: String 53 | 54 | /// Optional array of image data associated with the message. 55 | public let images: [Data]? 56 | 57 | /// Optional array of tool calls associated with the message 58 | public let toolCalls: [ToolCall]? 59 | 60 | /// The thinking process of the assistant when thinking is enabled 61 | public let thinking: String? 62 | 63 | /// Creates a new chat message. 64 | /// 65 | /// - Parameters: 66 | /// - role: The role of the message sender. 67 | /// - content: The content of the message. 68 | /// - images: Optional array of image data associated with the message. 69 | /// - toolCalls: Optional array of tool calls associated with the message. 70 | /// - thinking: The thinking process of the assistant when thinking is enabled. 71 | private init( 72 | role: Role, 73 | content: String, 74 | images: [Data]? = nil, 75 | toolCalls: [ToolCall]? = nil, 76 | thinking: String? = nil 77 | ) { 78 | self.role = role 79 | self.content = content 80 | self.images = images 81 | self.toolCalls = toolCalls 82 | self.thinking = thinking 83 | } 84 | 85 | /// Creates a system message. 86 | /// 87 | /// - Parameters: 88 | /// - content: The content of the message. 89 | /// - images: Optional array of image data associated with the message. 90 | /// - Returns: A new `Message` instance with the system role. 91 | public static func system( 92 | _ content: String, 93 | images: [Data]? = nil 94 | ) -> Message { 95 | return Message( 96 | role: .system, 97 | content: content, 98 | images: images 99 | ) 100 | } 101 | 102 | /// Creates a user message. 103 | /// 104 | /// - Parameters: 105 | /// - content: The content of the message. 106 | /// - images: Optional array of image data associated with the message. 107 | /// - Returns: A new `Message` instance with the user role. 108 | public static func user( 109 | _ content: String, 110 | images: [Data]? = nil 111 | ) -> Message { 112 | return Message( 113 | role: .user, 114 | content: content, 115 | images: images 116 | ) 117 | } 118 | 119 | /// Creates an assistant message. 120 | /// 121 | /// - Parameters: 122 | /// - content: The content of the message. 123 | /// - images: Optional array of image data associated with the message. 124 | /// - toolCalls: Optional array of tool calls associated with the message. 125 | /// - thinking: The thinking process of the assistant when thinking is enabled. 126 | /// - Returns: A new `Message` instance with the assistant role. 127 | public static func assistant( 128 | _ content: String, 129 | images: [Data]? = nil, 130 | toolCalls: [ToolCall]? = nil, 131 | thinking: String? = nil 132 | ) -> Message { 133 | return Message( 134 | role: .assistant, 135 | content: content, 136 | images: images, 137 | toolCalls: toolCalls, 138 | thinking: thinking 139 | ) 140 | } 141 | 142 | /// Creates a tool message. 143 | /// 144 | /// - Parameters: 145 | /// - content: The content of the message. 146 | /// - Returns: A new `Message` instance with the tool role. 147 | public static func tool( 148 | _ content: String 149 | ) -> Message { 150 | return Message(role: .tool, content: content) 151 | } 152 | } 153 | } 154 | 155 | // MARK: - Codable 156 | extension Chat.Message: Codable { 157 | private enum CodingKeys: String, CodingKey { 158 | case role 159 | case content 160 | case images 161 | case toolCalls = "tool_calls" 162 | case thinking 163 | } 164 | 165 | public func encode(to encoder: Encoder) throws { 166 | var container = encoder.container(keyedBy: CodingKeys.self) 167 | 168 | try container.encode(role, forKey: .role) 169 | try container.encode(content, forKey: .content) 170 | try container.encodeIfPresent(images, forKey: .images) 171 | try container.encodeIfPresent(toolCalls, forKey: .toolCalls) 172 | try container.encodeIfPresent(thinking, forKey: .thinking) 173 | } 174 | 175 | public init(from decoder: Decoder) throws { 176 | let container = try decoder.container(keyedBy: CodingKeys.self) 177 | 178 | role = try container.decode(Role.self, forKey: .role) 179 | content = try container.decode(String.self, forKey: .content) 180 | images = try container.decodeIfPresent([Data].self, forKey: .images) 181 | toolCalls = try container.decodeIfPresent([ToolCall].self, forKey: .toolCalls) 182 | thinking = try container.decodeIfPresent(String.self, forKey: .thinking) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Sources/Ollama/Model.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Date 2 | import class Foundation.ISO8601DateFormatter 3 | 4 | /// Namespace for model-related types and functionality. 5 | public enum Model { 6 | /// An identifier in the form of "[namespace/]model[:tag]". 7 | /// This structure is used to uniquely identify models in the Ollama ecosystem. 8 | public struct ID: Hashable, Equatable, Comparable, RawRepresentable, CustomStringConvertible, 9 | ExpressibleByStringLiteral, ExpressibleByStringInterpolation, Codable, Sendable 10 | { 11 | /// The optional namespace of the model. 12 | /// Namespaces are used to organize models, often representing the creator or organization. 13 | public let namespace: String? 14 | 15 | /// The name of the model. 16 | /// This is the primary identifier for the model. 17 | public let model: String 18 | 19 | /// The optional tag (version) of the model. 20 | /// Tags are used to specify different versions or variations of the same model. 21 | public let tag: String? 22 | 23 | /// The raw string representation of the model identifier. 24 | public typealias RawValue = String 25 | 26 | // MARK: Equatable & Comparable 27 | 28 | /// Compares two `Model.ID` instances for equality. 29 | /// The comparison is case-insensitive. 30 | public static func == (lhs: Model.ID, rhs: Model.ID) -> Bool { 31 | return lhs.rawValue.caseInsensitiveCompare(rhs.rawValue) == .orderedSame 32 | } 33 | 34 | /// Compares two `Model.ID` instances for ordering. 35 | /// The comparison is case-insensitive. 36 | public static func < (lhs: Model.ID, rhs: Model.ID) -> Bool { 37 | return lhs.rawValue.caseInsensitiveCompare(rhs.rawValue) == .orderedAscending 38 | } 39 | 40 | // MARK: RawRepresentable 41 | 42 | /// Initializes a `Model.ID` from a raw string value. 43 | /// The raw value should be in the format `"[namespace/]model[:tag]"`. 44 | public init?(rawValue: RawValue) { 45 | let components = rawValue.split(separator: "/", maxSplits: 1) 46 | 47 | if components.count == 2 { 48 | self.namespace = String(components[0]) 49 | let modelAndTag = components[1].split(separator: ":", maxSplits: 1) 50 | self.model = String(modelAndTag[0]) 51 | self.tag = modelAndTag.count > 1 ? String(modelAndTag[1]) : nil 52 | } else { 53 | self.namespace = nil 54 | let modelAndTag = rawValue.split(separator: ":", maxSplits: 1) 55 | self.model = String(modelAndTag[0]) 56 | self.tag = modelAndTag.count > 1 ? String(modelAndTag[1]) : nil 57 | } 58 | } 59 | 60 | /// Returns the raw string representation of the `Model.ID`. 61 | public var rawValue: String { 62 | let namespaceString = namespace.map { "\($0)/" } ?? "" 63 | let tagString = tag.map { ":\($0)" } ?? "" 64 | return "\(namespaceString)\(model)\(tagString)" 65 | } 66 | 67 | // MARK: CustomStringConvertible 68 | 69 | /// A textual representation of the `Model.ID`. 70 | public var description: String { 71 | return rawValue 72 | } 73 | 74 | // MARK: ExpressibleByStringLiteral 75 | 76 | /// Initializes a `Model.ID` from a string literal. 77 | public init(stringLiteral value: StringLiteralType) { 78 | self.init(rawValue: value)! 79 | } 80 | 81 | // MARK: ExpressibleByStringInterpolation 82 | 83 | /// Initializes a `Model.ID` from a string interpolation. 84 | public init(stringInterpolation: DefaultStringInterpolation) { 85 | self.init(rawValue: stringInterpolation.description)! 86 | } 87 | 88 | // MARK: Codable 89 | 90 | /// Decodes a `Model.ID` from a single string value. 91 | public init(from decoder: Decoder) throws { 92 | let container = try decoder.singleValueContainer() 93 | let rawValue = try container.decode(String.self) 94 | guard let identifier = Model.ID(rawValue: rawValue) else { 95 | throw DecodingError.dataCorruptedError( 96 | in: container, debugDescription: "Invalid Identifier string: \(rawValue)") 97 | } 98 | self = identifier 99 | } 100 | 101 | /// Encodes the `Model.ID` as a single string value. 102 | public func encode(to encoder: Encoder) throws { 103 | var container = encoder.singleValueContainer() 104 | try container.encode(rawValue) 105 | } 106 | 107 | // MARK: Pattern Matching 108 | 109 | /// Defines the pattern matching operator for `Model.ID`. 110 | /// This allows for partial matching based on namespace, model name, and tag. 111 | public static func ~= (pattern: Model.ID, value: Model.ID) -> Bool { 112 | if let patternNamespace = pattern.namespace, patternNamespace != value.namespace { 113 | return false 114 | } 115 | if pattern.model != value.model { 116 | return false 117 | } 118 | if let patternTag = pattern.tag, patternTag != value.tag { 119 | return false 120 | } 121 | return true 122 | } 123 | } 124 | 125 | // MARK: - 126 | 127 | /// Represents additional information about a model. 128 | public struct Details: Hashable, Codable, Sendable { 129 | /// The format of the model file (e.g., "gguf"). 130 | public let format: String 131 | 132 | /// The primary family or architecture of the model (e.g., "llama"). 133 | public let family: String 134 | 135 | /// Additional families or architectures the model belongs to, if any. 136 | public let families: [String]? 137 | 138 | /// The parameter size of the model (e.g., "7B", "13B"). 139 | public let parameterSize: String 140 | 141 | /// The quantization level of the model (e.g., "Q4_0"). 142 | public let quantizationLevel: String 143 | 144 | /// The parent model, if this model is derived from another. 145 | public let parentModel: String? 146 | 147 | /// Coding keys for mapping JSON keys to struct properties. 148 | enum CodingKeys: String, CodingKey { 149 | case format, family, families 150 | case parameterSize = "parameter_size" 151 | case quantizationLevel = "quantization_level" 152 | case parentModel = "parent_model" 153 | } 154 | 155 | /// Creates a model details object. 156 | /// - Parameters: 157 | /// - format: The format of the model file (e.g., "gguf"). 158 | /// - family: The primary family or architecture of the model (e.g., "llama"). 159 | /// - families: Additional families or architectures the model belongs to, if any. 160 | /// - parameterSize: The parameter size of the model (e.g., "7B", "13B"). 161 | /// - quantizationLevel: The quantization level of the model (e.g., "Q4_0"). 162 | /// - parentModel: The parent model, if this model is derived from another. 163 | public init( 164 | format: String, 165 | family: String, 166 | families: [String]? = nil, 167 | parameterSize: String, 168 | quantizationLevel: String, 169 | parentModel: String? = nil 170 | ) { 171 | self.format = format 172 | self.family = family 173 | self.families = families 174 | self.parameterSize = parameterSize 175 | self.quantizationLevel = quantizationLevel 176 | self.parentModel = parentModel 177 | } 178 | } 179 | 180 | /// Represents a capability that a model may support. 181 | public struct Capability: Hashable, Comparable, RawRepresentable, Sendable, Codable, 182 | ExpressibleByStringLiteral 183 | { 184 | public typealias RawValue = String 185 | 186 | public let rawValue: String 187 | 188 | public init(rawValue: String) { 189 | self.rawValue = rawValue 190 | } 191 | 192 | public init(stringLiteral value: StringLiteralType) { 193 | self.init(rawValue: value) 194 | } 195 | 196 | public static func < (lhs: Capability, rhs: Capability) -> Bool { 197 | return lhs.rawValue < rhs.rawValue 198 | } 199 | 200 | public init(from decoder: Decoder) throws { 201 | let container = try decoder.singleValueContainer() 202 | let rawValue = try container.decode(String.self) 203 | self.init(rawValue: rawValue) 204 | } 205 | 206 | public func encode(to encoder: Encoder) throws { 207 | var container = encoder.singleValueContainer() 208 | try container.encode(rawValue) 209 | } 210 | 211 | /// The ability to generate text completions based on a prompt. 212 | public static let completion: Capability = "completion" 213 | 214 | /// The ability to use tools and function calling capabilities. 215 | public static let tools: Capability = "tools" 216 | 217 | /// The ability to insert text at a specific position in the context. 218 | public static let insert: Capability = "insert" 219 | 220 | /// The ability to process and understand visual inputs. 221 | public static let vision: Capability = "vision" 222 | 223 | /// The ability to generate embeddings for text inputs. 224 | public static let embedding: Capability = "embedding" 225 | 226 | /// The ability to provide thinking/reasoning steps in the output. 227 | public static let thinking: Capability = "thinking" 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /Tests/OllamaTests/ToolTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | 4 | @testable import Ollama 5 | 6 | @Suite 7 | struct ToolTests { 8 | @Test 9 | func verifyToolSchema() throws { 10 | guard case let .object(schema) = hexColorTool.schemaValue else { 11 | Issue.record("Schema is not an object") 12 | return 13 | } 14 | 15 | // Verify basic schema structure 16 | #expect(schema["type"]?.stringValue == "function") 17 | 18 | guard let function = schema["function"]?.objectValue else { 19 | Issue.record("Missing or invalid function object in schema") 20 | return 21 | } 22 | 23 | #expect(function["name"]?.stringValue == "rgb_to_hex") 24 | #expect(function["description"]?.stringValue != nil) 25 | 26 | guard let parameters = function["parameters"]?.objectValue else { 27 | Issue.record("Missing or invalid parameters in schema") 28 | return 29 | } 30 | 31 | #expect(parameters["type"]?.stringValue == "object") 32 | 33 | guard let properties = parameters["properties"]?.objectValue else { 34 | Issue.record("Missing or invalid properties in parameters") 35 | return 36 | } 37 | #expect(properties.count == 3, "Expected 3 parameters, got \(properties.count)") 38 | 39 | // Check required parameter definitions and constraints 40 | for (key, value) in properties { 41 | guard let paramObj = value.objectValue else { 42 | Issue.record("Missing parameter object for \(key)") 43 | continue 44 | } 45 | 46 | #expect(paramObj["type"]?.stringValue == "number", "Invalid type for \(key)") 47 | #expect(paramObj["description"]?.stringValue != nil, "Missing description for \(key)") 48 | #expect(paramObj["minimum"]?.doubleValue == 0, "Invalid minimum for \(key)") 49 | #expect(paramObj["maximum"]?.doubleValue == 1, "Invalid maximum for \(key)") 50 | } 51 | 52 | #expect( 53 | parameters["required"]?.arrayValue == ["red", "green", "blue"], 54 | "Expected 3 required parameters, got \(parameters["required"]?.arrayValue ?? [])" 55 | ) 56 | } 57 | 58 | @Test 59 | func testInputSerialization() throws { 60 | let input = HexColorInput(red: 0.5, green: 0.7, blue: 0.9) 61 | 62 | // Test JSON encoding 63 | let encoder = JSONEncoder() 64 | let encoded = try encoder.encode(input) 65 | 66 | // Test JSON decoding 67 | let decoder = JSONDecoder() 68 | let decoded = try decoder.decode(HexColorInput.self, from: encoded) 69 | 70 | // Verify roundtrip 71 | #expect(decoded.red == input.red) 72 | #expect(decoded.green == input.green) 73 | #expect(decoded.blue == input.blue) 74 | } 75 | 76 | @Test 77 | func testColorConversion() async throws { 78 | // Test various color combinations 79 | let testCases = [ 80 | (red: 1.0, green: 0.0, blue: 0.0, expected: "#FF0000"), // Red 81 | (red: 0.0, green: 1.0, blue: 0.0, expected: "#00FF00"), // Green 82 | (red: 0.0, green: 0.0, blue: 1.0, expected: "#0000FF"), // Blue 83 | (red: 0.0, green: 0.0, blue: 0.0, expected: "#000000"), // Black 84 | (red: 1.0, green: 1.0, blue: 1.0, expected: "#FFFFFF"), // White 85 | (red: 0.5, green: 0.5, blue: 0.5, expected: "#808080"), // Gray 86 | ] 87 | 88 | for testCase in testCases { 89 | let input = HexColorInput( 90 | red: testCase.red, 91 | green: testCase.green, 92 | blue: testCase.blue 93 | ) 94 | let result = try await hexColorTool(input) 95 | #expect(result == testCase.expected, "Failed conversion for \(testCase)") 96 | } 97 | } 98 | 99 | @Test 100 | func testBackwardsCompatibilityWithFullSchema() throws { 101 | // Define a simple struct for testing 102 | struct TestInput: Codable { 103 | let query: String 104 | } 105 | 106 | // Create a tool using the old style (full schema in parameters) 107 | let oldStyleTool = Tool( 108 | name: "test_tool", 109 | description: "A test tool", 110 | parameters: [ 111 | "type": .string("object"), 112 | "properties": .object([ 113 | "query": .object([ 114 | "type": .string("string"), 115 | "description": .string("The search query"), 116 | ]) 117 | ]), 118 | "required": .array([.string("query")]), 119 | ] 120 | ) { (input: TestInput) async throws -> String in 121 | return "result" 122 | } 123 | 124 | // Create the same tool using the new style 125 | let newStyleTool = Tool( 126 | name: "test_tool", 127 | description: "A test tool", 128 | parameters: [ 129 | "query": .object([ 130 | "type": .string("string"), 131 | "description": .string("The search query"), 132 | ]) 133 | ], 134 | required: ["query"] 135 | ) { (input: TestInput) async throws -> String in 136 | return "result" 137 | } 138 | 139 | // Verify both tools generate the same schema 140 | guard case let .object(oldSchema) = oldStyleTool.schemaValue, 141 | case let .object(newSchema) = newStyleTool.schemaValue, 142 | let oldFunction = oldSchema["function"]?.objectValue, 143 | let newFunction = newSchema["function"]?.objectValue, 144 | let oldParameters = oldFunction["parameters"]?.objectValue, 145 | let newParameters = newFunction["parameters"]?.objectValue, 146 | let oldProperties = oldParameters["properties"]?.objectValue, 147 | let newProperties = newParameters["properties"]?.objectValue, 148 | let oldRequired = oldParameters["required"]?.arrayValue, 149 | let newRequired = newParameters["required"]?.arrayValue 150 | else { 151 | Issue.record("Invalid schema structure") 152 | return 153 | } 154 | 155 | // Compare the properties 156 | #expect(oldProperties.count == newProperties.count) 157 | #expect( 158 | oldProperties["query"]?.objectValue?["type"] 159 | == newProperties["query"]?.objectValue?["type"]) 160 | #expect( 161 | oldProperties["query"]?.objectValue?["description"] 162 | == newProperties["query"]?.objectValue?["description"]) 163 | 164 | // Compare the required fields 165 | #expect(oldRequired.count == newRequired.count) 166 | #expect(oldRequired[0] == newRequired[0]) 167 | } 168 | 169 | @Test 170 | func testBackwardsCompatibilityWithRequiredField() throws { 171 | // Define a simple struct for testing 172 | struct TestInput: Codable { 173 | let query: String 174 | } 175 | 176 | // Create a tool with a full schema but no explicit required parameter 177 | let toolWithImplicitRequired = Tool( 178 | name: "test_tool", 179 | description: "A test tool", 180 | parameters: [ 181 | "type": .string("object"), 182 | "properties": .object([ 183 | "query": .object([ 184 | "type": .string("string"), 185 | "description": .string("The search query"), 186 | ]) 187 | ]), 188 | "required": .array([.string("query")]), 189 | ] 190 | ) { (input: TestInput) async throws -> String in 191 | return "result" 192 | } 193 | 194 | // Create a tool with a full schema and an explicit required parameter (which should override) 195 | let toolWithExplicitRequired = Tool( 196 | name: "test_tool", 197 | description: "A test tool", 198 | parameters: [ 199 | "type": .string("object"), 200 | "properties": .object([ 201 | "query": .object([ 202 | "type": .string("string"), 203 | "description": .string("The search query"), 204 | ]) 205 | ]), 206 | "required": .array([.string("query")]), // This should be ignored 207 | ], 208 | required: ["differentField"] // This should take precedence 209 | ) { (input: TestInput) async throws -> String in 210 | return "result" 211 | } 212 | 213 | // Verify the required fields are correctly handled 214 | guard case let .object(implicitSchema) = toolWithImplicitRequired.schemaValue, 215 | case let .object(explicitSchema) = toolWithExplicitRequired.schemaValue, 216 | let implicitFunction = implicitSchema["function"]?.objectValue, 217 | let explicitFunction = explicitSchema["function"]?.objectValue, 218 | let implicitParameters = implicitFunction["parameters"]?.objectValue, 219 | let explicitParameters = explicitFunction["parameters"]?.objectValue, 220 | let implicitRequired = implicitParameters["required"]?.arrayValue, 221 | let explicitRequired = explicitParameters["required"]?.arrayValue 222 | else { 223 | Issue.record("Invalid schema structure") 224 | return 225 | } 226 | 227 | // For implicit required from schema, we should see "query" 228 | #expect(implicitRequired.count == 1) 229 | #expect(implicitRequired[0].stringValue == "query") 230 | 231 | // For explicit required parameter, we should see "differentField" 232 | #expect(explicitRequired.count == 1) 233 | #expect(explicitRequired[0].stringValue == "differentField") 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Tests/OllamaTests/ModelTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | 4 | @testable import Ollama 5 | 6 | @Suite 7 | struct ModelTests { 8 | @Suite 9 | struct IDTests { 10 | @Test 11 | func testModelIDInitialization() throws { 12 | // Test with real Ollama model identifiers 13 | 14 | // Full format with namespace 15 | guard let deepseekModel = Model.ID(rawValue: "deepseek/deepseek-r1:7b") else { 16 | Issue.record("Failed to create Model.ID with namespace") 17 | return 18 | } 19 | #expect(deepseekModel.namespace == "deepseek") 20 | #expect(deepseekModel.model == "deepseek-r1") 21 | #expect(deepseekModel.tag == "7b") 22 | 23 | // Model with tag (most common format on Ollama) 24 | guard let llama3Model = Model.ID(rawValue: "llama3:8b") else { 25 | Issue.record("Failed to create Model.ID with tag") 26 | return 27 | } 28 | #expect(llama3Model.namespace == nil) 29 | #expect(llama3Model.model == "llama3") 30 | #expect(llama3Model.tag == "8b") 31 | 32 | // Model without tag 33 | guard let mistralModel = Model.ID(rawValue: "mistral") else { 34 | Issue.record("Failed to create Model.ID without tag") 35 | return 36 | } 37 | #expect(mistralModel.namespace == nil) 38 | #expect(mistralModel.model == "mistral") 39 | #expect(mistralModel.tag == nil) 40 | 41 | // Vision model 42 | guard let llavaModel = Model.ID(rawValue: "llava:7b") else { 43 | Issue.record("Failed to create Model.ID for vision model") 44 | return 45 | } 46 | #expect(llavaModel.model == "llava") 47 | #expect(llavaModel.tag == "7b") 48 | 49 | // Embedding model 50 | guard let embedModel = Model.ID(rawValue: "nomic-embed-text") else { 51 | Issue.record("Failed to create Model.ID for embedding model") 52 | return 53 | } 54 | #expect(embedModel.model == "nomic-embed-text") 55 | #expect(embedModel.tag == nil) 56 | } 57 | 58 | @Test 59 | func testModelIDRawValue() throws { 60 | // Test with real Ollama models 61 | let qwen: Model.ID = "qwen2.5:72b" 62 | #expect(qwen.rawValue == "qwen2.5:72b") 63 | 64 | let gemma: Model.ID = "gemma2:27b" 65 | #expect(gemma.rawValue == "gemma2:27b") 66 | 67 | let codellama: Model.ID = "codellama:34b" 68 | #expect(codellama.rawValue == "codellama:34b") 69 | 70 | let phi4: Model.ID = "phi4:14b" 71 | #expect(phi4.rawValue == "phi4:14b") 72 | } 73 | 74 | @Test 75 | func testModelIDEquality() throws { 76 | // Test with real model names 77 | let llama1: Model.ID = "llama3.1:70b" 78 | let llama2: Model.ID = "llama3.1:70b" 79 | let llama3: Model.ID = "LLAMA3.1:70B" // Different case 80 | let llama4: Model.ID = "llama3.2:70b" // Different version 81 | 82 | // Test equality 83 | #expect(llama1 == llama2) 84 | #expect(llama1 == llama3) // Case-insensitive 85 | #expect(llama1 != llama4) // Different model 86 | 87 | // Test with embedding models 88 | let embed1: Model.ID = "mxbai-embed-large" 89 | let embed2: Model.ID = "MXBAI-EMBED-LARGE" 90 | #expect(embed1 == embed2) // Case-insensitive 91 | } 92 | 93 | @Test 94 | func testModelIDComparison() throws { 95 | // Test with real Ollama models 96 | let codellama: Model.ID = "codellama:7b" 97 | let llama2: Model.ID = "llama2:7b" 98 | let llama3: Model.ID = "llama3:7b" 99 | 100 | // Test comparison (alphabetical) 101 | #expect(codellama < llama2) 102 | #expect(llama2 < llama3) 103 | #expect(codellama < llama3) 104 | } 105 | 106 | @Test 107 | func testModelIDStringLiteral() throws { 108 | // Real Ollama models as string literals 109 | let mistral: Model.ID = "mistral:7b" 110 | #expect(mistral.model == "mistral") 111 | #expect(mistral.tag == "7b") 112 | 113 | let qwq: Model.ID = "qwq:32b" 114 | #expect(qwq.model == "qwq") 115 | #expect(qwq.tag == "32b") 116 | 117 | let deepseek: Model.ID = "deepseek-v3:671b" 118 | #expect(deepseek.model == "deepseek-v3") 119 | #expect(deepseek.tag == "671b") 120 | } 121 | 122 | @Test 123 | func testModelIDStringInterpolation() throws { 124 | // Build model IDs dynamically 125 | let model = "qwen2.5-coder" 126 | let size = "32b" 127 | 128 | let coder: Model.ID = "\(model):\(size)" 129 | #expect(coder.model == "qwen2.5-coder") 130 | #expect(coder.tag == "32b") 131 | } 132 | 133 | @Test 134 | func testModelIDDescription() throws { 135 | let dolphin: Model.ID = "dolphin3:8b" 136 | #expect(dolphin.description == "dolphin3:8b") 137 | 138 | let vision: Model.ID = "llama3.2-vision:11b" 139 | #expect(vision.description == "llama3.2-vision:11b") 140 | } 141 | 142 | @Test 143 | func testModelIDCodable() throws { 144 | let original: Model.ID = "llava:34b" 145 | 146 | // Test encoding 147 | let encoder = JSONEncoder() 148 | let encoded = try encoder.encode(original) 149 | let encodedString = String(data: encoded, encoding: .utf8)! 150 | #expect(encodedString == "\"llava:34b\"") 151 | 152 | // Test decoding 153 | let decoder = JSONDecoder() 154 | let decoded = try decoder.decode(Model.ID.self, from: encoded) 155 | #expect(decoded == original) 156 | #expect(decoded.model == "llava") 157 | #expect(decoded.tag == "34b") 158 | 159 | // Test array of models 160 | let models: [Model.ID] = ["llama3:8b", "mistral:7b", "gemma2:9b"] 161 | let encodedArray = try encoder.encode(models) 162 | let decodedArray = try decoder.decode([Model.ID].self, from: encodedArray) 163 | #expect(decodedArray.count == 3) 164 | #expect(decodedArray[0].model == "llama3") 165 | #expect(decodedArray[1].model == "mistral") 166 | #expect(decodedArray[2].model == "gemma2") 167 | } 168 | 169 | @Test 170 | func testModelIDPatternMatching() throws { 171 | let model: Model.ID = "qwen2.5:72b" 172 | 173 | // Test exact match 174 | let pattern1: Model.ID = "qwen2.5:72b" 175 | #expect(pattern1 ~= model) 176 | 177 | // Test pattern without tag (should match any tag) 178 | let pattern2: Model.ID = "qwen2.5" 179 | #expect(pattern2 ~= model) 180 | 181 | // Test non-matching patterns 182 | let pattern3: Model.ID = "qwen2:72b" // Different model version 183 | #expect(!(pattern3 ~= model)) 184 | 185 | let pattern4: Model.ID = "qwen2.5:7b" // Different tag 186 | #expect(!(pattern4 ~= model)) 187 | } 188 | 189 | @Test 190 | func testModelIDHashable() throws { 191 | // Test with popular Ollama models 192 | let llama3_8b: Model.ID = "llama3:8b" 193 | let llama3_8b_dup: Model.ID = "llama3:8b" 194 | let llama3_70b: Model.ID = "llama3:70b" 195 | 196 | // Test Set behavior 197 | var modelSet: Set = [] 198 | modelSet.insert(llama3_8b) 199 | modelSet.insert(llama3_8b_dup) // Should not increase count 200 | modelSet.insert(llama3_70b) 201 | 202 | #expect(modelSet.count == 2) 203 | #expect(modelSet.contains(llama3_8b)) 204 | #expect(modelSet.contains(llama3_70b)) 205 | 206 | // Test Dictionary usage 207 | var modelCapabilities: [Model.ID: [Model.Capability]] = [:] 208 | modelCapabilities["llava:7b"] = [.vision, .completion] 209 | modelCapabilities["nomic-embed-text"] = [.embedding] 210 | modelCapabilities["qwen3:8b"] = [.tools, .thinking] 211 | 212 | #expect(modelCapabilities.count == 3) 213 | #expect(modelCapabilities["llava:7b"]?.contains(.vision) == true) 214 | } 215 | 216 | @Test 217 | func testModelIDEdgeCases() throws { 218 | // Test with model names containing special characters 219 | guard let hyphenated = Model.ID(rawValue: "llama-guard3:8b") else { 220 | Issue.record("Failed to create Model.ID with hyphenated name") 221 | return 222 | } 223 | #expect(hyphenated.model == "llama-guard3") 224 | #expect(hyphenated.tag == "8b") 225 | 226 | // Test with version numbers in model names 227 | guard let versioned = Model.ID(rawValue: "qwen2.5-coder:32b") else { 228 | Issue.record("Failed to create Model.ID with versioned name") 229 | return 230 | } 231 | #expect(versioned.model == "qwen2.5-coder") 232 | #expect(versioned.tag == "32b") 233 | 234 | // Test with unusual tag formats 235 | guard let complexTag = Model.ID(rawValue: "mixtral:8x7b") else { 236 | Issue.record("Failed to create Model.ID with complex tag") 237 | return 238 | } 239 | #expect(complexTag.model == "mixtral") 240 | #expect(complexTag.tag == "8x7b") 241 | 242 | // Test with very large parameter counts 243 | guard let largeModel = Model.ID(rawValue: "command-a:111b") else { 244 | Issue.record("Failed to create Model.ID with large parameter count") 245 | return 246 | } 247 | #expect(largeModel.model == "command-a") 248 | #expect(largeModel.tag == "111b") 249 | } 250 | } 251 | 252 | @Suite 253 | struct CapabilityTests { 254 | @Test 255 | func testCapabilityInitialization() throws { 256 | // Test string literal initialization 257 | let completion: Model.Capability = "completion" 258 | #expect(completion.rawValue == "completion") 259 | 260 | // Test raw value initialization 261 | let tools = Model.Capability(rawValue: "tools") 262 | #expect(tools.rawValue == "tools") 263 | 264 | // Test predefined capabilities 265 | #expect(Model.Capability.completion.rawValue == "completion") 266 | #expect(Model.Capability.tools.rawValue == "tools") 267 | #expect(Model.Capability.insert.rawValue == "insert") 268 | #expect(Model.Capability.vision.rawValue == "vision") 269 | #expect(Model.Capability.embedding.rawValue == "embedding") 270 | #expect(Model.Capability.thinking.rawValue == "thinking") 271 | } 272 | 273 | @Test 274 | func testCapabilityComparison() throws { 275 | let completion: Model.Capability = "completion" 276 | let tools: Model.Capability = "tools" 277 | 278 | // Test equality 279 | #expect(completion == Model.Capability.completion) 280 | #expect(tools == Model.Capability.tools) 281 | #expect(completion != tools) 282 | 283 | // Test comparison 284 | #expect(completion < tools) // "completion" < "tools" alphabetically 285 | #expect(tools > completion) 286 | } 287 | 288 | @Test 289 | func testCapabilityCoding() throws { 290 | let capability: Model.Capability = "completion" 291 | 292 | // Test encoding 293 | let encoder = JSONEncoder() 294 | let encoded = try encoder.encode(capability) 295 | 296 | // Test decoding 297 | let decoder = JSONDecoder() 298 | let decoded = try decoder.decode(Model.Capability.self, from: encoded) 299 | 300 | // Verify roundtrip 301 | #expect(decoded == capability) 302 | #expect(decoded.rawValue == capability.rawValue) 303 | } 304 | 305 | @Test 306 | func testCustomCapability() throws { 307 | // Test creating a custom capability 308 | let custom: Model.Capability = "custom_feature" 309 | #expect(custom.rawValue == "custom_feature") 310 | 311 | // Test that custom capabilities work with comparison 312 | #expect(custom > Model.Capability.completion) // "custom_feature" > "completion" alphabetically 313 | #expect(custom < Model.Capability.vision) // "custom_feature" < "vision" alphabetically 314 | } 315 | 316 | @Test 317 | func testCapabilityCollection() throws { 318 | // Test using capabilities in collections 319 | let capabilities: Set = [ 320 | .completion, 321 | .tools, 322 | .insert, 323 | .vision, 324 | .embedding, 325 | .thinking, 326 | ] 327 | 328 | #expect(capabilities.count == 6) 329 | #expect(capabilities.contains(.completion)) 330 | #expect(capabilities.contains(.tools)) 331 | #expect(capabilities.contains(.insert)) 332 | #expect(capabilities.contains(.vision)) 333 | #expect(capabilities.contains(.embedding)) 334 | #expect(capabilities.contains(.thinking)) 335 | 336 | // Test array sorting 337 | let sorted = capabilities.sorted() 338 | #expect(sorted[0] == .completion) 339 | #expect(sorted[1] == .embedding) 340 | #expect(sorted[2] == .insert) 341 | #expect(sorted[3] == .thinking) 342 | #expect(sorted[4] == .tools) 343 | #expect(sorted[5] == .vision) 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /Sources/Ollama/Value.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Data 2 | import class Foundation.JSONDecoder 3 | import class Foundation.JSONEncoder 4 | 5 | /// A codable value. 6 | public enum Value: Hashable, Sendable { 7 | case null 8 | case bool(Bool) 9 | case int(Int) 10 | case double(Double) 11 | case string(String) 12 | case data(mimeType: String? = nil, Data) 13 | case array([Value]) 14 | case object([String: Value]) 15 | 16 | /// Create a `Value` from a `Codable` value. 17 | /// - Parameter value: The codable value 18 | /// - Returns: A value 19 | public init(_ value: T) throws { 20 | if let valueAsValue = value as? Value { 21 | self = valueAsValue 22 | } else { 23 | let data = try JSONEncoder().encode(value) 24 | self = try JSONDecoder().decode(Value.self, from: data) 25 | } 26 | } 27 | 28 | /// Returns whether the value is `null`. 29 | public var isNull: Bool { 30 | return self == .null 31 | } 32 | 33 | /// Returns the `Bool` value if the value is a `bool`, 34 | /// otherwise returns `nil`. 35 | public var boolValue: Bool? { 36 | guard case let .bool(value) = self else { return nil } 37 | return value 38 | } 39 | 40 | /// Returns the `Int` value if the value is an `integer`, 41 | /// otherwise returns `nil`. 42 | public var intValue: Int? { 43 | guard case let .int(value) = self else { return nil } 44 | return value 45 | } 46 | 47 | /// Returns the `Double` value if the value is a `double`, 48 | /// otherwise returns `nil`. 49 | public var doubleValue: Double? { 50 | switch self { 51 | case .double(let value): 52 | return value 53 | case .int(let value): 54 | return Double(value) 55 | default: 56 | return nil 57 | } 58 | } 59 | 60 | /// Returns the `String` value if the value is a `string`, 61 | /// otherwise returns `nil`. 62 | public var stringValue: String? { 63 | guard case let .string(value) = self else { return nil } 64 | return value 65 | } 66 | 67 | /// Returns the data value and optional MIME type if the value is `data`, 68 | /// otherwise returns `nil`. 69 | public var dataValue: (mimeType: String?, Data)? { 70 | guard case let .data(mimeType: mimeType, data) = self else { return nil } 71 | return (mimeType: mimeType, data) 72 | } 73 | 74 | /// Returns the `[Value]` value if the value is an `array`, 75 | /// otherwise returns `nil`. 76 | public var arrayValue: [Value]? { 77 | guard case let .array(value) = self else { return nil } 78 | return value 79 | } 80 | 81 | /// Returns the `[String: Value]` value if the value is an `object`, 82 | /// otherwise returns `nil`. 83 | public var objectValue: [String: Value]? { 84 | guard case let .object(value) = self else { return nil } 85 | return value 86 | } 87 | } 88 | 89 | // MARK: - Codable 90 | 91 | extension Value: Codable { 92 | public init(from decoder: Decoder) throws { 93 | let container = try decoder.singleValueContainer() 94 | 95 | if container.decodeNil() { 96 | self = .null 97 | } else if let value = try? container.decode(Bool.self) { 98 | self = .bool(value) 99 | } else if let value = try? container.decode(Int.self) { 100 | self = .int(value) 101 | } else if let value = try? container.decode(Double.self) { 102 | self = .double(value) 103 | } else if let value = try? container.decode(String.self) { 104 | if Data.isDataURL(string: value), 105 | case let (mimeType, data)? = Data.parseDataURL(value) 106 | { 107 | self = .data(mimeType: mimeType, data) 108 | } else { 109 | self = .string(value) 110 | } 111 | } else if let value = try? container.decode([Value].self) { 112 | self = .array(value) 113 | } else if let value = try? container.decode([String: Value].self) { 114 | self = .object(value) 115 | } else { 116 | throw DecodingError.dataCorruptedError( 117 | in: container, debugDescription: "Value type not found") 118 | } 119 | } 120 | 121 | public func encode(to encoder: Encoder) throws { 122 | var container = encoder.singleValueContainer() 123 | 124 | switch self { 125 | case .null: 126 | try container.encodeNil() 127 | case .bool(let value): 128 | try container.encode(value) 129 | case .int(let value): 130 | try container.encode(value) 131 | case .double(let value): 132 | try container.encode(value) 133 | case .string(let value): 134 | try container.encode(value) 135 | case let .data(mimeType, value): 136 | try container.encode(value.dataURLEncoded(mimeType: mimeType)) 137 | case .array(let value): 138 | try container.encode(value) 139 | case .object(let value): 140 | try container.encode(value) 141 | } 142 | } 143 | } 144 | 145 | extension Value: CustomStringConvertible { 146 | public var description: String { 147 | switch self { 148 | case .null: 149 | return "" 150 | case .bool(let value): 151 | return value.description 152 | case .int(let value): 153 | return value.description 154 | case .double(let value): 155 | return value.description 156 | case .string(let value): 157 | return value.description 158 | case let .data(mimeType, value): 159 | return value.dataURLEncoded(mimeType: mimeType) 160 | case .array(let value): 161 | return value.description 162 | case .object(let value): 163 | return value.description 164 | } 165 | } 166 | } 167 | 168 | // MARK: - ExpressibleByNilLiteral 169 | 170 | extension Value: ExpressibleByNilLiteral { 171 | public init(nilLiteral: ()) { 172 | self = .null 173 | } 174 | } 175 | 176 | // MARK: - ExpressibleByBooleanLiteral 177 | 178 | extension Value: ExpressibleByBooleanLiteral { 179 | public init(booleanLiteral value: Bool) { 180 | self = .bool(value) 181 | } 182 | } 183 | 184 | // MARK: - ExpressibleByIntegerLiteral 185 | 186 | extension Value: ExpressibleByIntegerLiteral { 187 | public init(integerLiteral value: Int) { 188 | self = .int(value) 189 | } 190 | } 191 | 192 | // MARK: - ExpressibleByFloatLiteral 193 | 194 | extension Value: ExpressibleByFloatLiteral { 195 | public init(floatLiteral value: Double) { 196 | self = .double(value) 197 | } 198 | } 199 | 200 | // MARK: - ExpressibleByStringLiteral 201 | 202 | extension Value: ExpressibleByStringLiteral { 203 | public init(stringLiteral value: String) { 204 | self = .string(value) 205 | } 206 | } 207 | 208 | // MARK: - ExpressibleByArrayLiteral 209 | 210 | extension Value: ExpressibleByArrayLiteral { 211 | public init(arrayLiteral elements: Value...) { 212 | self = .array(elements) 213 | } 214 | } 215 | 216 | // MARK: - ExpressibleByDictionaryLiteral 217 | 218 | extension Value: ExpressibleByDictionaryLiteral { 219 | public init(dictionaryLiteral elements: (String, Value)...) { 220 | var dictionary: [String: Value] = [:] 221 | for (key, value) in elements { 222 | dictionary[key] = value 223 | } 224 | self = .object(dictionary) 225 | } 226 | } 227 | 228 | // MARK: - ExpressibleByStringInterpolation 229 | 230 | extension Value: ExpressibleByStringInterpolation { 231 | public struct StringInterpolation: StringInterpolationProtocol { 232 | var stringValue: String 233 | 234 | public init(literalCapacity: Int, interpolationCount: Int) { 235 | self.stringValue = "" 236 | self.stringValue.reserveCapacity(literalCapacity + interpolationCount) 237 | } 238 | 239 | public mutating func appendLiteral(_ literal: String) { 240 | self.stringValue.append(literal) 241 | } 242 | 243 | public mutating func appendInterpolation(_ value: T) { 244 | self.stringValue.append(value.description) 245 | } 246 | } 247 | 248 | public init(stringInterpolation: StringInterpolation) { 249 | self = .string(stringInterpolation.stringValue) 250 | } 251 | } 252 | 253 | // MARK: - Standard Library Type Extensions 254 | 255 | extension Bool { 256 | /// Creates a boolean value from a `Value` instance. 257 | /// 258 | /// In strict mode, only `.bool` values are converted. In non-strict mode, the following conversions are supported: 259 | /// - Integers: `1` is `true`, `0` is `false` 260 | /// - Doubles: `1.0` is `true`, `0.0` is `false` 261 | /// - Strings (lowercase only): 262 | /// - `true`: "true", "t", "yes", "y", "on", "1" 263 | /// - `false`: "false", "f", "no", "n", "off", "0" 264 | /// 265 | /// - Parameters: 266 | /// - value: The `Value` to convert 267 | /// - strict: When `true`, only converts from `.bool` values. Defaults to `true` 268 | /// - Returns: A boolean value if conversion is possible, `nil` otherwise 269 | /// 270 | /// - Example: 271 | /// ```swift 272 | /// Bool(Value.bool(true)) // Returns true 273 | /// Bool(Value.int(1), strict: false) // Returns true 274 | /// Bool(Value.string("yes"), strict: false) // Returns true 275 | /// ``` 276 | public init?(_ value: Value, strict: Bool = true) { 277 | switch value { 278 | case .bool(let b): 279 | self = b 280 | case .int(let i) where !strict: 281 | switch i { 282 | case 0: self = false 283 | case 1: self = true 284 | default: return nil 285 | } 286 | case .double(let d) where !strict: 287 | switch d { 288 | case 0.0: self = false 289 | case 1.0: self = true 290 | default: return nil 291 | } 292 | case .string(let s) where !strict: 293 | switch s { 294 | case "true", "t", "yes", "y", "on", "1": 295 | self = true 296 | case "false", "f", "no", "n", "off", "0": 297 | self = false 298 | default: 299 | return nil 300 | } 301 | default: 302 | return nil 303 | } 304 | } 305 | } 306 | 307 | extension Int { 308 | /// Creates an integer value from a `Value` instance. 309 | /// 310 | /// In strict mode, only `.int` values are converted. In non-strict mode, the following conversions are supported: 311 | /// - Doubles: Converted if they can be represented exactly as integers 312 | /// - Strings: Parsed if they contain a valid integer representation 313 | /// 314 | /// - Parameters: 315 | /// - value: The `Value` to convert 316 | /// - strict: When `true`, only converts from `.int` values. Defaults to `true` 317 | /// - Returns: An integer value if conversion is possible, `nil` otherwise 318 | /// 319 | /// - Example: 320 | /// ```swift 321 | /// Int(Value.int(42)) // Returns 42 322 | /// Int(Value.double(42.0), strict: false) // Returns 42 323 | /// Int(Value.string("42"), strict: false) // Returns 42 324 | /// Int(Value.double(42.5), strict: false) // Returns nil 325 | /// ``` 326 | public init?(_ value: Value, strict: Bool = true) { 327 | switch value { 328 | case .int(let i): 329 | self = i 330 | case .double(let d) where !strict: 331 | guard let intValue = Int(exactly: d) else { return nil } 332 | self = intValue 333 | case .string(let s) where !strict: 334 | guard let intValue = Int(s) else { return nil } 335 | self = intValue 336 | default: 337 | return nil 338 | } 339 | } 340 | } 341 | 342 | extension Double { 343 | /// Creates a double value from a `Value` instance. 344 | /// 345 | /// In strict mode, converts from `.double` and `.int` values. In non-strict mode, the following conversions are supported: 346 | /// - Integers: Converted to their double representation 347 | /// - Strings: Parsed if they contain a valid floating-point representation 348 | /// 349 | /// - Parameters: 350 | /// - value: The `Value` to convert 351 | /// - strict: When `true`, only converts from `.double` and `.int` values. Defaults to `true` 352 | /// - Returns: A double value if conversion is possible, `nil` otherwise 353 | /// 354 | /// - Example: 355 | /// ```swift 356 | /// Double(Value.double(42.5)) // Returns 42.5 357 | /// Double(Value.int(42)) // Returns 42.0 358 | /// Double(Value.string("42.5"), strict: false) // Returns 42.5 359 | /// ``` 360 | public init?(_ value: Value, strict: Bool = true) { 361 | switch value { 362 | case .double(let d): 363 | self = d 364 | case .int(let i): 365 | self = Double(i) 366 | case .string(let s) where !strict: 367 | guard let doubleValue = Double(s) else { return nil } 368 | self = doubleValue 369 | default: 370 | return nil 371 | } 372 | } 373 | } 374 | 375 | extension String { 376 | /// Creates a string value from a `Value` instance. 377 | /// 378 | /// In strict mode, only `.string` values are converted. In non-strict mode, the following conversions are supported: 379 | /// - Integers: Converted to their string representation 380 | /// - Doubles: Converted to their string representation 381 | /// - Booleans: Converted to "true" or "false" 382 | /// 383 | /// - Parameters: 384 | /// - value: The `Value` to convert 385 | /// - strict: When `true`, only converts from `.string` values. Defaults to `true` 386 | /// - Returns: A string value if conversion is possible, `nil` otherwise 387 | /// 388 | /// - Example: 389 | /// ```swift 390 | /// String(Value.string("hello")) // Returns "hello" 391 | /// String(Value.int(42), strict: false) // Returns "42" 392 | /// String(Value.bool(true), strict: false) // Returns "true" 393 | /// ``` 394 | public init?(_ value: Value, strict: Bool = true) { 395 | switch value { 396 | case .string(let s): 397 | self = s 398 | case .int(let i) where !strict: 399 | self = String(i) 400 | case .double(let d) where !strict: 401 | self = String(d) 402 | case .bool(let b) where !strict: 403 | self = String(b) 404 | default: 405 | return nil 406 | } 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ollama Swift Client 2 | 3 | A Swift client library for interacting with the 4 | [Ollama API](https://github.com/ollama/ollama/blob/main/docs/api.md). 5 | 6 | ## Requirements 7 | 8 | - Swift 5.7+ 9 | - macOS 13+ 10 | - [Ollama](https://ollama.com) 11 | 12 | ## Installation 13 | 14 | ### Swift Package Manager 15 | 16 | Add the following to your `Package.swift` file: 17 | 18 | ```swift 19 | .package(url: "https://github.com/mattt/ollama-swift.git", from: "1.8.0") 20 | ``` 21 | 22 | ## Usage 23 | 24 | > [!NOTE] 25 | > The tests and example code for this library use the 26 | > [llama3.2](https://ollama.com/library/llama3.2) model. 27 | > Run the following command to download the model to run them yourself: 28 | > 29 | > ``` 30 | > ollama pull llama3.2 31 | > ``` 32 | 33 | ### Initializing the client 34 | 35 | ```swift 36 | import Ollama 37 | 38 | // Use the default client (http://localhost:11434) 39 | let client = Client.default 40 | 41 | // Or create a custom client 42 | let customClient = Client(host: URL(string: "http://your-ollama-host:11434")!, userAgent: "MyApp/1.0") 43 | ``` 44 | 45 | ### Generating text 46 | 47 | Generate text using a specified model: 48 | 49 | ```swift 50 | do { 51 | let response = try await client.generate( 52 | model: "llama3.2", 53 | prompt: "Tell me a joke about Swift programming.", 54 | options: [ 55 | "temperature": 0.7, 56 | "max_tokens": 100 57 | ], 58 | keepAlive: .minutes(10) // Keep model loaded for 10 minutes 59 | ) 60 | print(response.response) 61 | } catch { 62 | print("Error: \(error)") 63 | } 64 | ``` 65 | 66 | #### Streaming text generation 67 | 68 | Generate text in a streaming fashion to receive responses in real-time: 69 | 70 | ```swift 71 | do { 72 | let stream = try await client.generateStream( 73 | model: "llama3.2", 74 | prompt: "Tell me a joke about Swift programming.", 75 | options: [ 76 | "temperature": 0.7, 77 | "max_tokens": 100 78 | ] 79 | ) 80 | 81 | var fullResponse = "" 82 | for try await chunk in stream { 83 | // Process each chunk of the response as it arrives 84 | print(chunk.response, terminator: "") 85 | fullResponse += chunk.response 86 | } 87 | print("\nFull response: \(fullResponse)") 88 | } catch { 89 | print("Error: \(error)") 90 | } 91 | ``` 92 | 93 | ### Chatting with a model 94 | 95 | Generate a chat completion: 96 | 97 | ```swift 98 | do { 99 | let response = try await client.chat( 100 | model: "llama3.2", 101 | messages: [ 102 | .system("You are a helpful assistant."), 103 | .user("In which city is Apple Inc. located?") 104 | ], 105 | keepAlive: .minutes(10) // Keep model loaded for 10 minutes 106 | ) 107 | print(response.message.content) 108 | } catch { 109 | print("Error: \(error)") 110 | } 111 | ``` 112 | 113 | #### Streaming chat responses 114 | 115 | Stream chat responses to get real-time partial completions: 116 | 117 | ```swift 118 | do { 119 | let stream = try await client.chatStream( 120 | model: "llama3.2", 121 | messages: [ 122 | .system("You are a helpful assistant."), 123 | .user("Write a short poem about Swift programming.") 124 | ] 125 | ) 126 | 127 | var fullContent = "" 128 | for try await chunk in stream { 129 | // Process each chunk of the message as it arrives 130 | if let content = chunk.message.content { 131 | print(content, terminator: "") 132 | fullContent += content 133 | } 134 | } 135 | print("\nComplete poem: \(fullContent)") 136 | } catch { 137 | print("Error: \(error)") 138 | } 139 | ``` 140 | 141 | You can also stream chat responses when using tools: 142 | 143 | ```swift 144 | do { 145 | let stream = try await client.chatStream( 146 | model: "llama3.2", 147 | messages: [ 148 | .system("You are a helpful assistant that can check the weather."), 149 | .user("What's the weather like in Portland?") 150 | ], 151 | tools: [weatherTool] 152 | ) 153 | 154 | for try await chunk in stream { 155 | // Check if the model is making tool calls 156 | if let toolCalls = chunk.message.toolCalls, !toolCalls.isEmpty { 157 | print("Model is requesting tool: \(toolCalls[0].function.name)") 158 | } 159 | 160 | // Print content from the message as it streams 161 | if let content = chunk.message.content { 162 | print(content, terminator: "") 163 | } 164 | 165 | // Check if this is the final chunk 166 | if chunk.done { 167 | print("\nResponse complete") 168 | } 169 | } 170 | } catch { 171 | print("Error: \(error)") 172 | } 173 | ``` 174 | 175 | ### Using Structured Outputs 176 | 177 | You can request structured outputs from models by specifying a format. 178 | Pass `"json"` to get back a JSON string, 179 | or specify a full [JSON Schema](https://json-schema.org): 180 | 181 | ```swift 182 | // Simple JSON format 183 | let response = try await client.chat( 184 | model: "llama3.2", 185 | messages: [.user("List 3 colors.")], 186 | format: "json" 187 | ) 188 | 189 | // Using JSON schema for more control 190 | let schema: Value = [ 191 | "type": "object", 192 | "properties": [ 193 | "colors": [ 194 | "type": "array", 195 | "items": [ 196 | "type": "object", 197 | "properties": [ 198 | "name": ["type": "string"], 199 | "hex": ["type": "string"] 200 | ], 201 | "required": ["name", "hex"] 202 | ] 203 | ] 204 | ], 205 | "required": ["colors"] 206 | ] 207 | 208 | let response = try await client.chat( 209 | model: "llama3.2", 210 | messages: [.user("List 3 colors with their hex codes.")], 211 | format: schema 212 | ) 213 | 214 | // The response will be a JSON object matching the schema: 215 | // { 216 | // "colors": [ 217 | // {"name": "papayawhip", "hex": "#FFEFD5"}, 218 | // {"name": "indigo", "hex": "#4B0082"}, 219 | // {"name": "navy", "hex": "#000080"} 220 | // ] 221 | // } 222 | ``` 223 | 224 | The format parameter works with both `chat` and `generate` methods. 225 | 226 | ### Using Thinking Models 227 | 228 | Some models support a "thinking" mode 229 | where they show their reasoning process before providing the final answer. 230 | This is particularly useful for complex reasoning tasks. 231 | 232 | ```swift 233 | // Generate with thinking enabled 234 | let response = try await client.generate( 235 | model: "deepseek-r1:8b", 236 | prompt: "What is 17 * 23? Show your work.", 237 | think: true 238 | ) 239 | 240 | print("Thinking: \(response.thinking ?? "None")") 241 | print("Answer: \(response.response)") 242 | ``` 243 | 244 | You can also use thinking in chat conversations: 245 | 246 | ```swift 247 | let response = try await client.chat( 248 | model: "deepseek-r1:8b", 249 | messages: [ 250 | .system("You are a helpful mathematician."), 251 | .user("Calculate 9.9 + 9.11 and explain your reasoning.") 252 | ], 253 | think: true 254 | ) 255 | 256 | print("Thinking: \(response.message.thinking ?? "None")") 257 | print("Response: \(response.message.content)") 258 | ``` 259 | 260 | > [!TIP] 261 | > You can check which models support thinking by examining their capabilities: 262 | > ```swift 263 | > let modelInfo = try await client.showModel("deepseek-r1:8b") 264 | > if modelInfo.capabilities.contains(.thinking) { 265 | > print("🧠 This model supports thinking!") 266 | > } 267 | > ``` 268 | 269 | ### Managing Model Memory with Keep-Alive 270 | 271 | You can control how long a model stays loaded in memory using the `keepAlive` parameter. This is useful for managing memory usage and response times. 272 | 273 | ```swift 274 | // Use server default (typically 5 minutes) 275 | let response = try await client.generate( 276 | model: "llama3.2", 277 | prompt: "Hello!" 278 | // keepAlive defaults to .default 279 | ) 280 | 281 | // Keep model loaded for 10 minutes 282 | let response = try await client.generate( 283 | model: "llama3.2", 284 | prompt: "Hello!", 285 | keepAlive: .minutes(10) 286 | ) 287 | 288 | // Keep model loaded for 2 hours 289 | let response = try await client.chat( 290 | model: "llama3.2", 291 | messages: [.user("Hello!")], 292 | keepAlive: .hours(2) 293 | ) 294 | 295 | // Keep model loaded for 30 seconds 296 | let response = try await client.generate( 297 | model: "llama3.2", 298 | prompt: "Hello!", 299 | keepAlive: .seconds(30) 300 | ) 301 | 302 | // Keep model loaded indefinitely 303 | let response = try await client.chat( 304 | model: "llama3.2", 305 | messages: [.user("Hello!")], 306 | keepAlive: .forever 307 | ) 308 | 309 | // Unload model immediately after response 310 | let response = try await client.generate( 311 | model: "llama3.2", 312 | prompt: "Hello!", 313 | keepAlive: .none 314 | ) 315 | ``` 316 | 317 | - **`.default`** - Use the server's default keep-alive behavior (default if not specified) 318 | - **`.none`** - Unload immediately after the request 319 | - **`.seconds(Int)`** - Keep loaded for the specified number of seconds 320 | - **`.minutes(Int)`** - Keep loaded for the specified number of minutes 321 | - **`.hours(Int)`** - Keep loaded for the specified number of hours 322 | - **`.forever`** - Keep loaded indefinitely 323 | 324 | > [!NOTE] 325 | > Zero durations (e.g., `.seconds(0)`) are treated as `.none` (unload immediately). 326 | > Negative durations are treated as `.forever` (keep loaded indefinitely). 327 | 328 | ### Using Tools 329 | 330 | Ollama supports tool calling with models, 331 | allowing models to perform complex tasks or interact with external services. 332 | 333 | > [!NOTE] 334 | > Tool support requires a [compatible model](https://ollama.com/search?c=tools), 335 | > such as llama3.2. 336 | 337 | #### Creating a Tool 338 | 339 | Define a tool by specifying its name, description, parameters, and implementation: 340 | 341 | ```swift 342 | struct WeatherInput: Codable { 343 | let city: String 344 | } 345 | 346 | struct WeatherOutput: Codable { 347 | let temperature: Double 348 | let conditions: String 349 | } 350 | 351 | let weatherTool = Tool( 352 | name: "get_current_weather", 353 | description: """ 354 | Get the current weather for a city, 355 | with conditions ("sunny", "cloudy", etc.) 356 | and temperature in °C. 357 | """, 358 | parameters: [ 359 | "city": [ 360 | "type": "string", 361 | "description": "The city to get weather for" 362 | ] 363 | ], 364 | required: ["city"] 365 | ) { input async throws -> WeatherOutput in 366 | // Implement weather lookup logic here 367 | return WeatherOutput(temperature: 18.5, conditions: "cloudy") 368 | } 369 | ``` 370 | 371 | > [!IMPORTANT] 372 | > In version 1.3.0 and later, 373 | > the `parameters` argument should contain only the properties object, 374 | > not the full JSON schema of the tool. 375 | > 376 | > For backward compatibility, 377 | > passing a full schema in the `parameters` argument 378 | > (with `"type"`, `"properties"`, and `"required"` fields) 379 | > is still supported but deprecated and will emit a warning in debug builds. 380 | > 381 | >
382 | > Click to see code examples of old vs. new format 383 | > 384 | > ```swift 385 | > // ✅ New format 386 | > let weatherTool = Tool( 387 | > name: "get_current_weather", 388 | > description: "Get the current weather for a city", 389 | > parameters: [ 390 | > "city": [ 391 | > "type": "string", 392 | > "description": "The city to get weather for" 393 | > ] 394 | > ], 395 | > required: ["city"] 396 | > ) { /* implementation */ } 397 | > 398 | > // ❌ Deprecated format (still works but not recommended) 399 | > let weatherTool = Tool( 400 | > name: "get_current_weather", 401 | > description: "Get the current weather for a city", 402 | > parameters: [ 403 | > "type": "object", 404 | > "properties": [ 405 | > "city": [ 406 | > "type": "string", 407 | > "description": "The city to get weather for" 408 | > ] 409 | > ], 410 | > "required": ["city"] 411 | > ] 412 | > ) { /* implementation */ } 413 | > ``` 414 | >
415 | 416 | #### Using Tools in Chat 417 | 418 | Provide tools to the model during chat: 419 | 420 | ```swift 421 | let messages: [Chat.Message] = [ 422 | .system("You are a helpful assistant that can check the weather."), 423 | .user("What's the weather like in Portland?") 424 | ] 425 | 426 | let response = try await client.chat( 427 | model: "llama3.1", 428 | messages: messages, 429 | tools: [weatherTool] 430 | ) 431 | 432 | // Handle tool calls in the response 433 | if let toolCalls = response.message.toolCalls { 434 | for toolCall in toolCalls { 435 | print("Tool called: \(toolCall.function.name)") 436 | print("Arguments: \(toolCall.function.arguments)") 437 | } 438 | } 439 | ``` 440 | 441 | #### Multi-turn Tool Conversations 442 | 443 | Tools can be used in multi-turn conversations, where the model can use tool results to provide more detailed responses: 444 | 445 | ```swift 446 | var messages: [Chat.Message] = [ 447 | .system("You are a helpful assistant that can convert colors."), 448 | .user("What's the hex code for yellow?") 449 | ] 450 | 451 | // First turn - model calls the tool 452 | let response1 = try await client.chat( 453 | model: "llama3.1", 454 | messages: messages, 455 | tools: [rgbToHexTool] 456 | ) 457 | 458 | enum ToolError { 459 | case invalidParameters 460 | } 461 | 462 | // Add tool response to conversation 463 | if let toolCall = response1.message.toolCalls?.first { 464 | // Parse the tool arguments 465 | guard let args = toolCall.function.arguments, 466 | let redValue = args["red"], 467 | let greenValue = args["green"], 468 | let blueValue = args["blue"], 469 | let red = Double(redValue, strict: false), 470 | let green = Double(greenValue, strict: false), 471 | let blue = Double(blueValue, strict: false) 472 | else { 473 | throw ToolError.invalidParameters 474 | } 475 | 476 | let input = HexColorInput( 477 | red: red, 478 | green: green, 479 | blue: blue 480 | ) 481 | 482 | // Execute the tool with the input 483 | let hexColor = try await rgbToHexTool(input) 484 | 485 | // Add the tool result to the conversation 486 | messages.append(.tool(hexColor)) 487 | } 488 | 489 | // Continue conversation with tool result 490 | messages.append(.user("What other colors are similar?")) 491 | let response2 = try await client.chat( 492 | model: "llama3.1", 493 | messages: messages, 494 | tools: [rgbToHexTool] 495 | ) 496 | ``` 497 | 498 | ### Generating embeddings 499 | 500 | Generate embeddings for a given text: 501 | 502 | ```swift 503 | do { 504 | let response = try await client.embed( 505 | model: "llama3.2", 506 | input: "Here is an article about llamas..." 507 | ) 508 | print("Embeddings: \(response.embeddings)") 509 | } catch { 510 | print("Error: \(error)") 511 | } 512 | ``` 513 | 514 | Generate embeddings for multiple texts in a single batch: 515 | 516 | ```swift 517 | do { 518 | let texts = [ 519 | "First article about llamas...", 520 | "Second article about alpacas...", 521 | "Third article about vicuñas..." 522 | ] 523 | 524 | let response = try await client.embed( 525 | model: "llama3.2", 526 | inputs: texts 527 | ) 528 | 529 | // Access embeddings for each input 530 | for (index, embedding) in response.embeddings.rawValue.enumerated() { 531 | print("Embedding \(index): \(embedding.count) dimensions") 532 | } 533 | } catch { 534 | print("Error: \(error)") 535 | } 536 | ``` 537 | 538 | ### Managing models 539 | 540 | #### Listing models 541 | 542 | List available models: 543 | 544 | ```swift 545 | do { 546 | let models = try await client.listModels() 547 | for model in models { 548 | print("Model: \(model.name), Modified: \(model.modifiedAt)") 549 | } 550 | } catch { 551 | print("Error: \(error)") 552 | } 553 | ``` 554 | 555 | #### Retrieving model information 556 | 557 | Get detailed information about a specific model: 558 | 559 | ```swift 560 | do { 561 | let modelInfo = try await client.showModel("llama3.2") 562 | print("Modelfile: \(modelInfo.modelfile)") 563 | print("Parameters: \(modelInfo.parameters)") 564 | print("Template: \(modelInfo.template)") 565 | } catch { 566 | print("Error: \(error)") 567 | } 568 | ``` 569 | 570 | #### Pulling a model 571 | 572 | Download a model from the Ollama library: 573 | 574 | ```swift 575 | do { 576 | let success = try await client.pullModel("llama3.2") 577 | if success { 578 | print("Model successfully pulled") 579 | } else { 580 | print("Failed to pull model") 581 | } 582 | } catch { 583 | print("Error: \(error)") 584 | } 585 | ``` 586 | 587 | #### Pushing a model 588 | 589 | ```swift 590 | do { 591 | let success = try await client.pushModel("mynamespace/mymodel:latest") 592 | if success { 593 | print("Model successfully pushed") 594 | } else { 595 | print("Failed to push model") 596 | } 597 | } catch { 598 | print("Error: \(error)") 599 | } 600 | ``` 601 | 602 | ## License 603 | 604 | This project is available under the MIT license. 605 | See the LICENSE file for more info. 606 | -------------------------------------------------------------------------------- /Tests/OllamaTests/KeepAliveTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | 4 | @testable import Ollama 5 | 6 | @Suite 7 | struct KeepAliveTests { 8 | @Test 9 | func testKeepAliveConvenienceInitializers() throws { 10 | let tenSeconds: KeepAlive = .seconds(10) 11 | let thirtyMinutes: KeepAlive = .minutes(30) 12 | let twoHours: KeepAlive = .hours(2) 13 | 14 | // Verify structure 15 | switch tenSeconds { 16 | case .duration(.seconds(let value)): 17 | #expect(value == 10) 18 | default: 19 | Issue.record("Expected duration(.seconds(10))") 20 | } 21 | 22 | switch thirtyMinutes { 23 | case .duration(.minutes(let value)): 24 | #expect(value == 30) 25 | default: 26 | Issue.record("Expected duration(.minutes(30))") 27 | } 28 | 29 | switch twoHours { 30 | case .duration(.hours(let value)): 31 | #expect(value == 2) 32 | default: 33 | Issue.record("Expected duration(.hours(2))") 34 | } 35 | } 36 | 37 | @Test 38 | func testKeepAliveValueConversion() throws { 39 | // Test default 40 | let defaultKeepAlive: KeepAlive = .default 41 | let defaultValue = defaultKeepAlive.value 42 | #expect(defaultValue == nil) 43 | 44 | // Test none 45 | let none: KeepAlive = .none 46 | let noneValue = none.value 47 | if case .int(let val) = noneValue { 48 | #expect(val == 0) 49 | } else { 50 | Issue.record("Expected none to convert to .int(0)") 51 | } 52 | 53 | // Test forever 54 | let forever: KeepAlive = .forever 55 | let foreverValue = forever.value 56 | if case .int(let val) = foreverValue { 57 | #expect(val == -1) 58 | } else { 59 | Issue.record("Expected forever to convert to .int(-1)") 60 | } 61 | 62 | // Test duration 63 | let fiveMinutes: KeepAlive = .minutes(5) 64 | let fiveMinutesValue = fiveMinutes.value 65 | if case .string(let val) = fiveMinutesValue { 66 | #expect(val == "5m") 67 | } else { 68 | Issue.record("Expected 5 minutes to convert to .string(\"5m\")") 69 | } 70 | 71 | let tenSeconds: KeepAlive = .seconds(10) 72 | let tenSecondsValue = tenSeconds.value 73 | if case .string(let val) = tenSecondsValue { 74 | #expect(val == "10s") 75 | } else { 76 | Issue.record("Expected 10 seconds to convert to .string(\"10s\")") 77 | } 78 | 79 | let twoHours: KeepAlive = .hours(2) 80 | let twoHoursValue = twoHours.value 81 | if case .string(let val) = twoHoursValue { 82 | #expect(val == "2h") 83 | } else { 84 | Issue.record("Expected 2 hours to convert to .string(\"2h\")") 85 | } 86 | } 87 | 88 | @Test 89 | func testKeepAliveZeroDurations() throws { 90 | let none: KeepAlive = .none 91 | let zeroSeconds: KeepAlive = .seconds(0) 92 | let zeroMinutes: KeepAlive = .minutes(0) 93 | let zeroHours: KeepAlive = .hours(0) 94 | 95 | // Test value conversion 96 | if case .int(let val) = zeroSeconds.value { 97 | #expect(val == 0) 98 | } else { 99 | Issue.record("Expected 0 seconds to convert to .int(0)") 100 | } 101 | 102 | if case .int(let val) = zeroMinutes.value { 103 | #expect(val == 0) 104 | } else { 105 | Issue.record("Expected 0 minutes to convert to .int(0)") 106 | } 107 | 108 | if case .int(let val) = zeroHours.value { 109 | #expect(val == 0) 110 | } else { 111 | Issue.record("Expected 0 hours to convert to .int(0)") 112 | } 113 | 114 | // Test equality 115 | #expect(none.value == zeroSeconds.value) 116 | #expect(none.value == zeroMinutes.value) 117 | #expect(none.value == zeroHours.value) 118 | } 119 | 120 | @Test 121 | func testKeepAliveNegativeDurations() throws { 122 | let forever: KeepAlive = .forever 123 | let negativeSeconds: KeepAlive = .seconds(-5) 124 | let negativeMinutes: KeepAlive = .minutes(-5) 125 | let negativeHours: KeepAlive = .hours(-5) 126 | 127 | // Test value conversion 128 | if case .int(let val) = negativeSeconds.value { 129 | #expect(val == -1) 130 | } else { 131 | Issue.record("Expected negative seconds to convert to .int(-1)") 132 | } 133 | 134 | if case .int(let val) = negativeMinutes.value { 135 | #expect(val == -1) 136 | } else { 137 | Issue.record("Expected negative minutes to convert to .int(-1)") 138 | } 139 | 140 | if case .int(let val) = negativeHours.value { 141 | #expect(val == -1) 142 | } else { 143 | Issue.record("Expected negative hours to convert to .int(-1)") 144 | } 145 | 146 | // Test equality 147 | #expect(forever.value == negativeSeconds.value) 148 | #expect(forever.value == negativeMinutes.value) 149 | #expect(forever.value == negativeHours.value) 150 | } 151 | 152 | @Test 153 | func testKeepAliveDescription() throws { 154 | // Test default, none and forever 155 | let defaultKeepAlive: KeepAlive = .default 156 | #expect(defaultKeepAlive.description == "default") 157 | 158 | let none: KeepAlive = .none 159 | #expect(none.description == "none") 160 | 161 | let forever: KeepAlive = .forever 162 | #expect(forever.description == "forever") 163 | 164 | // Test duration descriptions (API format) 165 | let fiveMinutes: KeepAlive = .minutes(5) 166 | #expect(fiveMinutes.description == "5m") 167 | 168 | let tenSeconds: KeepAlive = .seconds(10) 169 | #expect(tenSeconds.description == "10s") 170 | 171 | let twoHours: KeepAlive = .hours(2) 172 | #expect(twoHours.description == "2h") 173 | } 174 | 175 | @Test 176 | func testKeepAliveEquality() throws { 177 | // Test basic equality 178 | let default1: KeepAlive = .default 179 | let default2: KeepAlive = .default 180 | #expect(default1 == default2) 181 | 182 | let none1: KeepAlive = .none 183 | let none2: KeepAlive = .none 184 | #expect(none1 == none2) 185 | 186 | let forever1: KeepAlive = .forever 187 | let forever2: KeepAlive = .forever 188 | #expect(forever1 == forever2) 189 | 190 | // Test duration equality 191 | let fiveMinutes1: KeepAlive = .minutes(5) 192 | let fiveMinutes2: KeepAlive = .minutes(5) 193 | #expect(fiveMinutes1 == fiveMinutes2) 194 | 195 | // Test inequality 196 | #expect(default1 != none1) 197 | #expect(default1 != forever1) 198 | #expect(default1 != fiveMinutes1) 199 | #expect(none1 != forever1) 200 | #expect(none1 != fiveMinutes1) 201 | #expect(forever1 != fiveMinutes1) 202 | 203 | let tenMinutes: KeepAlive = .minutes(10) 204 | #expect(fiveMinutes1 != tenMinutes) 205 | } 206 | 207 | @Test 208 | func testKeepAliveHashable() throws { 209 | // Test Set behavior 210 | var keepAliveSet: Set = [] 211 | keepAliveSet.insert(.default) 212 | keepAliveSet.insert(.default) // Should not increase count 213 | keepAliveSet.insert(.none) 214 | keepAliveSet.insert(.none) // Should not increase count 215 | keepAliveSet.insert(.forever) 216 | keepAliveSet.insert(.minutes(5)) 217 | keepAliveSet.insert(.minutes(5)) // Should not increase count 218 | keepAliveSet.insert(.minutes(10)) 219 | 220 | #expect(keepAliveSet.count == 5) 221 | #expect(keepAliveSet.contains(.default)) 222 | #expect(keepAliveSet.contains(.none)) 223 | #expect(keepAliveSet.contains(.forever)) 224 | #expect(keepAliveSet.contains(.minutes(5))) 225 | #expect(keepAliveSet.contains(.minutes(10))) 226 | } 227 | 228 | @Suite 229 | struct DurationTests { 230 | @Test 231 | func testDurationInitialization() throws { 232 | let seconds: KeepAlive.Duration = .seconds(30) 233 | let minutes: KeepAlive.Duration = .minutes(5) 234 | let hours: KeepAlive.Duration = .hours(2) 235 | 236 | // Test structure 237 | if case .seconds(let value) = seconds { 238 | #expect(value == 30) 239 | } else { 240 | Issue.record("Expected .seconds(30)") 241 | } 242 | 243 | if case .minutes(let value) = minutes { 244 | #expect(value == 5) 245 | } else { 246 | Issue.record("Expected .minutes(5)") 247 | } 248 | 249 | if case .hours(let value) = hours { 250 | #expect(value == 2) 251 | } else { 252 | Issue.record("Expected .hours(2)") 253 | } 254 | } 255 | 256 | @Test 257 | func testDurationDescription() throws { 258 | let seconds: KeepAlive.Duration = .seconds(30) 259 | #expect(seconds.description == "30s") 260 | 261 | let minutes: KeepAlive.Duration = .minutes(5) 262 | #expect(minutes.description == "5m") 263 | 264 | let hours: KeepAlive.Duration = .hours(2) 265 | #expect(hours.description == "2h") 266 | } 267 | 268 | @Test 269 | func testDurationEquality() throws { 270 | let fiveMinutes1: KeepAlive.Duration = .minutes(5) 271 | let fiveMinutes2: KeepAlive.Duration = .minutes(5) 272 | #expect(fiveMinutes1 == fiveMinutes2) 273 | 274 | let tenMinutes: KeepAlive.Duration = .minutes(10) 275 | #expect(fiveMinutes1 != tenMinutes) 276 | 277 | let threeHundredSeconds: KeepAlive.Duration = .seconds(300) 278 | // Note: 5 minutes = 300 seconds, but these are different cases so not equal 279 | #expect(fiveMinutes1 != threeHundredSeconds) 280 | } 281 | 282 | @Test 283 | func testDurationHashable() throws { 284 | var durationSet: Set = [] 285 | durationSet.insert(.seconds(30)) 286 | durationSet.insert(.seconds(30)) // Should not increase count 287 | durationSet.insert(.minutes(5)) 288 | durationSet.insert(.hours(2)) 289 | 290 | #expect(durationSet.count == 3) 291 | #expect(durationSet.contains(.seconds(30))) 292 | #expect(durationSet.contains(.minutes(5))) 293 | #expect(durationSet.contains(.hours(2))) 294 | } 295 | 296 | @Test 297 | func testKeepAliveComparison() throws { 298 | let defaultKeepAlive: KeepAlive = .default 299 | let none: KeepAlive = .none 300 | let fiveMinutes: KeepAlive = .minutes(5) 301 | let tenMinutes: KeepAlive = .minutes(10) 302 | let oneHour: KeepAlive = .hours(1) 303 | let forever: KeepAlive = .forever 304 | 305 | // Test default is smallest 306 | #expect(defaultKeepAlive < none) 307 | #expect(defaultKeepAlive < fiveMinutes) 308 | #expect(defaultKeepAlive < tenMinutes) 309 | #expect(defaultKeepAlive < oneHour) 310 | #expect(defaultKeepAlive < forever) 311 | 312 | // Test none is second smallest 313 | #expect(none < fiveMinutes) 314 | #expect(none < tenMinutes) 315 | #expect(none < oneHour) 316 | #expect(none < forever) 317 | 318 | // Test duration comparisons 319 | #expect(fiveMinutes < tenMinutes) 320 | #expect(tenMinutes < oneHour) 321 | #expect(fiveMinutes < oneHour) 322 | 323 | // Test reflexivity 324 | #expect(!(defaultKeepAlive < defaultKeepAlive)) 325 | #expect(!(none < none)) 326 | #expect(!(fiveMinutes < fiveMinutes)) 327 | #expect(!(forever < forever)) 328 | } 329 | 330 | @Test 331 | func testDurationComparison() throws { 332 | let thirtySeconds: KeepAlive.Duration = .seconds(30) 333 | let oneMinute: KeepAlive.Duration = .minutes(1) 334 | let ninetySeconds: KeepAlive.Duration = .seconds(90) 335 | let twoMinutes: KeepAlive.Duration = .minutes(2) 336 | let oneHour: KeepAlive.Duration = .hours(1) 337 | 338 | // Test basic comparisons 339 | #expect(thirtySeconds < oneMinute) // 30s < 60s 340 | #expect(oneMinute < ninetySeconds) // 60s < 90s 341 | #expect(ninetySeconds < twoMinutes) // 90s < 120s 342 | #expect(twoMinutes < oneHour) // 120s < 3600s 343 | 344 | // Test cross-unit comparisons 345 | #expect(thirtySeconds < twoMinutes) 346 | #expect(oneMinute < oneHour) 347 | 348 | // Test same values in different units 349 | let sixtySeconds: KeepAlive.Duration = .seconds(60) 350 | #expect(!(oneMinute < sixtySeconds)) // 60s == 60s (same total duration) 351 | #expect(!(sixtySeconds < oneMinute)) // 60s == 60s (same total duration) 352 | // Note: These are different enum cases, so they're not equal (==), but they compare as equivalent for ordering 353 | } 354 | 355 | @Test 356 | func testKeepAliveSorting() throws { 357 | let values: [KeepAlive] = [ 358 | .forever, 359 | .hours(2), 360 | .none, 361 | .minutes(30), 362 | .default, 363 | .minutes(5), 364 | .seconds(45), 365 | ] 366 | 367 | let sorted = values.sorted() 368 | 369 | // Expected order: default < none < seconds < minutes < hours < forever 370 | #expect(sorted[0] == .default) 371 | #expect(sorted[1] == .none) 372 | #expect(sorted[2] == .seconds(45)) 373 | #expect(sorted[3] == .minutes(5)) 374 | #expect(sorted[4] == .minutes(30)) 375 | #expect(sorted[5] == .hours(2)) 376 | #expect(sorted[6] == .forever) 377 | } 378 | 379 | @Test 380 | func testDurationSorting() throws { 381 | let durations: [KeepAlive.Duration] = [ 382 | .hours(1), 383 | .seconds(30), 384 | .minutes(5), 385 | .seconds(90), 386 | .minutes(1), 387 | .hours(2), 388 | ] 389 | 390 | let sorted = durations.sorted() 391 | 392 | // Expected order by total seconds: 30s, 60s, 90s, 300s, 3600s, 7200s 393 | #expect(sorted[0] == .seconds(30)) // 30s 394 | #expect(sorted[1] == .minutes(1)) // 60s 395 | #expect(sorted[2] == .seconds(90)) // 90s 396 | #expect(sorted[3] == .minutes(5)) // 300s 397 | #expect(sorted[4] == .hours(1)) // 3600s 398 | #expect(sorted[5] == .hours(2)) // 7200s 399 | } 400 | 401 | @Test 402 | func testZeroDurations() throws { 403 | // Test comparison - all zero durations should be equal in comparison 404 | let zeroSecondsDuration: KeepAlive.Duration = .seconds(0) 405 | let zeroMinutesDuration: KeepAlive.Duration = .minutes(0) 406 | let zeroHoursDuration: KeepAlive.Duration = .hours(0) 407 | 408 | #expect(!(zeroSecondsDuration < zeroMinutesDuration)) 409 | #expect(!(zeroMinutesDuration < zeroSecondsDuration)) 410 | #expect(!(zeroSecondsDuration < zeroHoursDuration)) 411 | #expect(!(zeroHoursDuration < zeroSecondsDuration)) 412 | } 413 | 414 | @Test 415 | func testLargeDurations() throws { 416 | let largeSeconds: KeepAlive = .seconds(999999) 417 | let largeMinutes: KeepAlive = .minutes(99999) 418 | let largeHours: KeepAlive = .hours(9999) 419 | 420 | // Test value conversion 421 | if case .string(let val) = largeSeconds.value { 422 | #expect(val == "999999s") 423 | } else { 424 | Issue.record("Expected large seconds to convert properly") 425 | } 426 | 427 | if case .string(let val) = largeHours.value { 428 | #expect(val == "9999h") 429 | } else { 430 | Issue.record("Expected large hours to convert properly") 431 | } 432 | 433 | // Test comparison still works with large values 434 | #expect(largeSeconds < largeMinutes) // seconds < minutes in total time 435 | } 436 | 437 | @Test 438 | func testNegativeDurations() throws { 439 | // Note: Negative durations are converted to forever (.int(-1)) 440 | let negativeSeconds: KeepAlive = .seconds(-5) 441 | let negativeMinutes: KeepAlive = .minutes(-1) 442 | 443 | // Test value conversion - negative durations become forever 444 | if case .int(let val) = negativeSeconds.value { 445 | #expect(val == -1) 446 | } else { 447 | Issue.record("Expected negative seconds to convert to .int(-1)") 448 | } 449 | 450 | if case .int(let val) = negativeMinutes.value { 451 | #expect(val == -1) 452 | } else { 453 | Issue.record("Expected negative minutes to convert to .int(-1)") 454 | } 455 | 456 | // Test comparison with negative values 457 | let negativeSecondsDuration: KeepAlive.Duration = .seconds(-5) 458 | let negativeMinutesDuration: KeepAlive.Duration = .minutes(-1) 459 | let positiveSecondsDuration: KeepAlive.Duration = .seconds(5) 460 | 461 | #expect(negativeSecondsDuration < positiveSecondsDuration) 462 | #expect(negativeMinutesDuration < negativeSecondsDuration) // -60s < -5s 463 | } 464 | 465 | @Test 466 | func testMixedComparisons() throws { 467 | // Test edge cases for cross-unit comparisons 468 | let almostOneMinute: KeepAlive.Duration = .seconds(59) 469 | let exactlyOneMinute: KeepAlive.Duration = .minutes(1) 470 | let justOverOneMinute: KeepAlive.Duration = .seconds(61) 471 | 472 | #expect(almostOneMinute < exactlyOneMinute) // 59s < 60s 473 | #expect(exactlyOneMinute < justOverOneMinute) // 60s < 61s 474 | #expect(almostOneMinute < justOverOneMinute) // 59s < 61s 475 | 476 | // Test hour/minute boundaries 477 | let almostOneHour: KeepAlive.Duration = .minutes(59) 478 | let exactlyOneHour: KeepAlive.Duration = .hours(1) 479 | let justOverOneHour: KeepAlive.Duration = .minutes(61) 480 | 481 | #expect(almostOneHour < exactlyOneHour) // 3540s < 3600s 482 | #expect(exactlyOneHour < justOverOneHour) // 3600s < 3660s 483 | } 484 | } 485 | } 486 | -------------------------------------------------------------------------------- /Tests/OllamaTests/ClientTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | 4 | @testable import Ollama 5 | 6 | @Suite( 7 | .serialized, 8 | .disabled(if: ProcessInfo.processInfo.environment["CI"] != nil) 9 | ) 10 | struct ClientTests { 11 | let ollama: Client 12 | 13 | init() async { 14 | ollama = await Client(host: Client.defaultHost) 15 | } 16 | 17 | @Test 18 | func testGenerateWithImage() async throws { 19 | // Create a transparent 1x1 pixel image 20 | let imageData = Data( 21 | base64Encoded: 22 | "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=" 23 | )! 24 | let prompt = "Describe this image in detail." 25 | 26 | let response = try await ollama.generate( 27 | model: "llama3.2", 28 | prompt: prompt, 29 | images: [imageData] 30 | ) 31 | 32 | #expect(!response.response.isEmpty) 33 | #expect(response.done) 34 | #expect(response.model == "llama3.2") 35 | #expect(response.totalDuration ?? 0 > 0) 36 | #expect(response.loadDuration ?? 0 > 0) 37 | #expect(response.promptEvalCount ?? 0 > 0) 38 | } 39 | 40 | @Test 41 | func testGenerateStream() async throws { 42 | let response = await ollama.generateStream( 43 | model: "llama3.2", 44 | prompt: "Write a haiku about llamas." 45 | ) 46 | 47 | var collect: [String] = [] 48 | for try await res in response { 49 | collect.append(res.response) 50 | } 51 | 52 | #expect(!collect.isEmpty) 53 | } 54 | 55 | @Test 56 | func testChatCompletion() async throws { 57 | let messages: [Chat.Message] = [ 58 | .system("You are a helpful AI assistant."), 59 | .user("Write a haiku about llamas."), 60 | ] 61 | 62 | let response = try await ollama.chat( 63 | model: "llama3.2", 64 | messages: messages) 65 | #expect(!response.message.content.isEmpty) 66 | } 67 | 68 | @Test 69 | func testChatStream() async throws { 70 | let messages: [Chat.Message] = [ 71 | .system("You are a helpful AI assistant."), 72 | .user("Write a haiku about llamas."), 73 | ] 74 | 75 | let response = try await ollama.chatStream( 76 | model: "llama3.2", 77 | messages: messages) 78 | 79 | var collect: [String] = [] 80 | for try await res in response { 81 | collect.append(res.message.content) 82 | } 83 | 84 | #expect(!collect.isEmpty) 85 | } 86 | 87 | @Test 88 | func testEmbed() async throws { 89 | let input = "This is a test sentence for embedding." 90 | let response = try await ollama.embed(model: "llama3.2", input: input) 91 | 92 | #expect(!response.embeddings.rawValue.isEmpty) 93 | #expect(response.totalDuration > 0) 94 | #expect(response.loadDuration > 0) 95 | #expect(response.promptEvalCount > 0) 96 | } 97 | 98 | @Test 99 | func testBatchEmbed() async throws { 100 | let inputs = [ 101 | "This is the first test sentence.", 102 | "This is the second test sentence.", 103 | "This is the third test sentence.", 104 | ] 105 | let response = try await ollama.embed(model: "llama3.2", inputs: inputs) 106 | 107 | #expect(response.embeddings.rawValue.count == inputs.count) 108 | #expect(!response.embeddings.rawValue.isEmpty) 109 | #expect(response.totalDuration > 0) 110 | #expect(response.loadDuration > 0) 111 | #expect(response.promptEvalCount > 0) 112 | 113 | // Verify each embedding is non-empty 114 | for embedding in response.embeddings.rawValue { 115 | #expect(!embedding.isEmpty) 116 | } 117 | } 118 | 119 | @Test 120 | func testListModels() async throws { 121 | let response = try await ollama.listModels() 122 | 123 | #expect(!response.models.isEmpty) 124 | #expect(response.models.first != nil) 125 | } 126 | 127 | @Test 128 | func testListRunningModels() async throws { 129 | let _ = try await ollama.listRunningModels() 130 | } 131 | 132 | @Test 133 | func testVersion() async throws { 134 | let response = try await ollama.version() 135 | 136 | #expect(!response.version.isEmpty) 137 | #expect(response.version.contains(".")) 138 | } 139 | 140 | @Test(.disabled()) 141 | func testCreateShowDeleteModel() async throws { 142 | let base = "llama3.2" 143 | let name: Model.ID = "test-\(UUID().uuidString)" 144 | let modelfile = 145 | """ 146 | FROM \(base) 147 | PARAMETER temperature 0.7 148 | """ 149 | 150 | // Create model 151 | var success = try await ollama.createModel(name: name, modelfile: modelfile) 152 | #expect(success) 153 | 154 | // Show model 155 | let response = try await ollama.showModel(name) 156 | #expect(response.details.parentModel?.hasPrefix(base + ":") ?? false) 157 | 158 | // Delete model 159 | success = try await ollama.deleteModel(name) 160 | #expect(success) 161 | 162 | // Verify deletion 163 | do { 164 | _ = try await ollama.showModel(name) 165 | Issue.record("Model should have been deleted") 166 | } catch { 167 | // Expected error 168 | } 169 | } 170 | 171 | @Test 172 | func testGenerateWithFormat() async throws { 173 | // Test string format 174 | do { 175 | let response = try await ollama.generate( 176 | model: "llama3.2", 177 | prompt: "List 3 colors and their hex codes.", 178 | format: "json" 179 | ) 180 | 181 | #expect(!response.response.isEmpty) 182 | #expect(response.done) 183 | 184 | // Verify response is valid JSON 185 | let data = response.response.data(using: .utf8)! 186 | let _ = try JSONSerialization.jsonObject(with: data) 187 | } 188 | 189 | // Test JSON schema format 190 | do { 191 | let schema: Value = [ 192 | "type": "object", 193 | "properties": [ 194 | "colors": [ 195 | "type": "array", 196 | "items": [ 197 | "type": "object", 198 | "properties": [ 199 | "name": ["type": "string"], 200 | "hex": ["type": "string"], 201 | ], 202 | "required": ["name", "hex"], 203 | ], 204 | ] 205 | ], 206 | "required": ["colors"], 207 | ] 208 | 209 | let response = try await ollama.generate( 210 | model: "llama3.2", 211 | prompt: "List 3 colors and their hex codes.", 212 | format: schema 213 | ) 214 | 215 | #expect(!response.response.isEmpty) 216 | #expect(response.done) 217 | 218 | // Verify response matches schema 219 | let data = response.response.data(using: .utf8)! 220 | let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] 221 | #expect(json["colors"] is [[String: String]]) 222 | } 223 | } 224 | 225 | @Test 226 | func testChatWithFormat() async throws { 227 | let messages: [Chat.Message] = [ 228 | .system("You are a helpful AI assistant."), 229 | .user("List 3 programming languages and when they were created."), 230 | ] 231 | 232 | // Test string format 233 | do { 234 | let response = try await ollama.chat( 235 | model: "llama3.2", 236 | messages: messages, 237 | format: "json" 238 | ) 239 | 240 | #expect(!response.message.content.isEmpty) 241 | 242 | // Verify response is valid JSON 243 | let data = response.message.content.data(using: .utf8)! 244 | let _ = try JSONSerialization.jsonObject(with: data) 245 | } 246 | 247 | // Test JSON schema format 248 | do { 249 | let schema: Value = [ 250 | "type": "object", 251 | "properties": [ 252 | "languages": [ 253 | "type": "array", 254 | "items": [ 255 | "type": "object", 256 | "properties": [ 257 | "name": ["type": "string"], 258 | "year": ["type": "integer"], 259 | "creator": ["type": "string"], 260 | ], 261 | "required": ["name", "year"], 262 | ], 263 | ] 264 | ], 265 | "required": ["languages"], 266 | ] 267 | 268 | let response = try await ollama.chat( 269 | model: "llama3.2", 270 | messages: messages, 271 | format: schema 272 | ) 273 | 274 | #expect(!response.message.content.isEmpty) 275 | 276 | // Verify response matches schema 277 | let data = response.message.content.data(using: .utf8)! 278 | let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] 279 | #expect(json["languages"] is [[String: Any]]) 280 | } 281 | } 282 | 283 | @Test 284 | func testChatWithTool() async throws { 285 | let messages: [Chat.Message] = [ 286 | .system( 287 | """ 288 | You are a helpful AI assistant that can convert colors. 289 | When asked about colors, use the rgb_to_hex tool to convert them. 290 | """), 291 | .user("What's the hex code for yellow?"), 292 | ] 293 | 294 | let response = try await ollama.chat( 295 | model: "llama3.2", 296 | messages: messages, 297 | tools: [hexColorTool] 298 | ) 299 | 300 | #expect(response.message.toolCalls?.count == 1) 301 | guard let toolCall = response.message.toolCalls?.first else { 302 | Issue.record("No tool call found in response") 303 | return 304 | } 305 | 306 | #expect(toolCall.function.name == "rgb_to_hex") 307 | let args = toolCall.function.arguments 308 | guard let red = Double(args["red"]!, strict: false), 309 | let green = Double(args["green"]!, strict: false), 310 | let blue = Double(args["blue"]!, strict: false) 311 | else { 312 | Issue.record("Failed to convert arguments to Double") 313 | return 314 | } 315 | #expect(red == 1.0) 316 | #expect(green == 1.0) 317 | #expect(blue == 0.0) 318 | } 319 | 320 | @Test 321 | func testChatStreamWithTool() async throws { 322 | let messages: [Chat.Message] = [ 323 | .system( 324 | """ 325 | You are a helpful AI assistant that can convert colors. 326 | When asked about colors, use the rgb_to_hex tool to convert them. 327 | """), 328 | .user("What's the hex code for yellow?"), 329 | ] 330 | 331 | let stream = try await ollama.chatStream( 332 | model: "llama3.2", 333 | messages: messages, 334 | tools: [hexColorTool] 335 | ) 336 | 337 | var foundToolCall = false 338 | 339 | for try await res in stream { 340 | // Check for tool calls in the message 341 | if let toolCalls = res.message.toolCalls, 342 | let toolCall = toolCalls.first, 343 | toolCall.function.name == "rgb_to_hex" 344 | { 345 | foundToolCall = true 346 | 347 | // Check if we can get the color values 348 | if let redValue = toolCall.function.arguments["red"], 349 | let greenValue = toolCall.function.arguments["green"], 350 | let blueValue = toolCall.function.arguments["blue"] 351 | { 352 | // Try to convert to Double and validate 353 | if let redDouble = Double(redValue, strict: false), 354 | let greenDouble = Double(greenValue, strict: false), 355 | let blueDouble = Double(blueValue, strict: false) 356 | { 357 | // Verify yellow color values (1.0, 1.0, 0.0) 358 | #expect(redDouble == 1.0, "Invalid red value: \(redDouble)") 359 | #expect(greenDouble == 1.0, "Invalid green value: \(greenDouble)") 360 | #expect(blueDouble == 0.0, "Invalid blue value: \(blueDouble)") 361 | } 362 | } 363 | } 364 | } 365 | 366 | #expect(foundToolCall, "No tool call found in any stream message") 367 | } 368 | 369 | @Test 370 | func testGenerateWithThinking() async throws { 371 | // Test with thinking enabled (using deepseek-r1 which supports thinking) 372 | do { 373 | let response = try await ollama.generate( 374 | model: "deepseek-r1:8b", 375 | prompt: "What is 9.9 + 9.11? Think about this carefully.", 376 | think: true 377 | ) 378 | 379 | #expect(!response.response.isEmpty) 380 | #expect(response.done) 381 | #expect(response.thinking != nil) 382 | #expect(!response.thinking!.isEmpty) 383 | #expect(response.model.rawValue.contains("deepseek-r1")) 384 | } catch { 385 | // Model might not be available, skip this test 386 | print("Skipping thinking test: \(error)") 387 | } 388 | } 389 | 390 | @Test 391 | func testGenerateStreamWithThinking() async throws { 392 | // Test streaming with thinking enabled 393 | do { 394 | let stream = await ollama.generateStream( 395 | model: "deepseek-r1:8b", 396 | prompt: "Count from 1 to 5. Show your reasoning.", 397 | think: true 398 | ) 399 | 400 | var responses: [Client.GenerateResponse] = [] 401 | var foundThinking = false 402 | 403 | for try await res in stream { 404 | responses.append(res) 405 | if res.thinking != nil && !res.thinking!.isEmpty { 406 | foundThinking = true 407 | } 408 | } 409 | 410 | #expect(!responses.isEmpty) 411 | #expect(foundThinking, "Expected to find thinking content in stream") 412 | } catch { 413 | // Model might not be available, skip this test 414 | print("Skipping thinking stream test: \(error)") 415 | } 416 | } 417 | 418 | @Test 419 | func testGenerateWithoutThinking() async throws { 420 | // Test with thinking explicitly disabled 421 | let response = try await ollama.generate( 422 | model: "llama3.2", 423 | prompt: "What is 2 + 2?", 424 | think: false 425 | ) 426 | 427 | #expect(!response.response.isEmpty) 428 | #expect(response.done) 429 | // Should not have thinking content when disabled 430 | #expect(response.thinking == nil) 431 | } 432 | 433 | @Test 434 | func testChatWithThinking() async throws { 435 | let messages: [Chat.Message] = [ 436 | .system("You are a helpful mathematician."), 437 | .user("What is 17 * 23? Please show your reasoning step by step."), 438 | ] 439 | 440 | // Test with thinking enabled (using deepseek-r1 which supports thinking) 441 | do { 442 | let response = try await ollama.chat( 443 | model: "deepseek-r1:8b", 444 | messages: messages, 445 | think: true 446 | ) 447 | 448 | #expect(!response.message.content.isEmpty) 449 | #expect(response.message.role == .assistant) 450 | #expect(response.message.thinking != nil) 451 | #expect(!response.message.thinking!.isEmpty) 452 | } catch { 453 | // Model might not be available, skip this test 454 | print("Skipping chat thinking test: \(error)") 455 | } 456 | } 457 | 458 | @Test 459 | func testChatStreamWithThinking() async throws { 460 | let messages: [Chat.Message] = [ 461 | .system("You are a helpful assistant."), 462 | .user( 463 | "Solve this riddle: I have cities, but no houses. I have mountains, but no trees. What am I?" 464 | ), 465 | ] 466 | 467 | // Test streaming with thinking enabled 468 | do { 469 | let stream = try await ollama.chatStream( 470 | model: "deepseek-r1:8b", 471 | messages: messages, 472 | think: true 473 | ) 474 | 475 | var responses: [Client.ChatResponse] = [] 476 | var foundThinking = false 477 | 478 | for try await res in stream { 479 | responses.append(res) 480 | if res.message.thinking != nil && !res.message.thinking!.isEmpty { 481 | foundThinking = true 482 | } 483 | } 484 | 485 | #expect(!responses.isEmpty) 486 | #expect(foundThinking, "Expected to find thinking content in chat stream") 487 | } catch { 488 | // Model might not be available, skip this test 489 | print("Skipping chat thinking stream test: \(error)") 490 | } 491 | } 492 | 493 | @Test 494 | func testChatWithoutThinking() async throws { 495 | let messages: [Chat.Message] = [ 496 | .system("You are a helpful assistant."), 497 | .user("What is the capital of France?"), 498 | ] 499 | 500 | // Test with thinking explicitly disabled 501 | let response = try await ollama.chat( 502 | model: "llama3.2", 503 | messages: messages, 504 | think: false 505 | ) 506 | 507 | #expect(!response.message.content.isEmpty) 508 | #expect(response.message.role == .assistant) 509 | // Should not have thinking content when disabled 510 | #expect(response.message.thinking == nil) 511 | } 512 | 513 | @Test 514 | func testChatWithToolsMultipleTurns() async throws { 515 | enum ColorError: Error { 516 | case unknownColor(String) 517 | } 518 | 519 | // Create a color name mapping tool 520 | let colorNameTool = Tool( 521 | name: "lookup_color", 522 | description: "Gets the RGB values (0-1) for common HTML color names", 523 | parameters: [ 524 | "colorName": [ 525 | "type": "string", 526 | "description": "Name of the HTML color", 527 | ] 528 | ], 529 | required: ["colorName"] 530 | ) { colorName in 531 | let colors: [String: HexColorInput] = [ 532 | "papayawhip": .init(red: 1.0, green: 0.937, blue: 0.835), 533 | "cornflowerblue": .init(red: 0.392, green: 0.584, blue: 0.929), 534 | "mediumseagreen": .init(red: 0.235, green: 0.702, blue: 0.443), 535 | ] 536 | 537 | guard let color = colors[colorName.lowercased()] else { 538 | throw ColorError.unknownColor(colorName) 539 | } 540 | return color 541 | } 542 | 543 | // First request - get RGB values 544 | var messages: [Chat.Message] = [ 545 | .system( 546 | """ 547 | You are a helpful AI assistant that can help with color conversions. 548 | First, use the lookup_color tool to get RGB values for color names. 549 | """), 550 | .user("What is the RGB for papayawhip?"), 551 | ] 552 | 553 | var response: Client.ChatResponse 554 | 555 | // First turn 556 | do { 557 | response = try await ollama.chat( 558 | model: "llama3.2", 559 | messages: messages, 560 | tools: [colorNameTool] 561 | ) 562 | 563 | // Verify color tool call 564 | #expect(response.message.toolCalls?.count == 1) 565 | guard let colorCall = response.message.toolCalls?.first else { 566 | Issue.record("Missing color tool call") 567 | return 568 | } 569 | #expect(colorCall.function.name == "lookup_color") 570 | 571 | guard let colorName = colorCall.function.arguments["colorName"]?.stringValue else { 572 | Issue.record("Missing color name") 573 | return 574 | } 575 | #expect(colorName == "papayawhip") 576 | 577 | let color = try await colorNameTool(colorName) 578 | guard let colorJSON = String(data: try JSONEncoder().encode(color), encoding: .utf8) 579 | else { 580 | Issue.record("Failed to encode color") 581 | return 582 | } 583 | messages.append(.tool(colorJSON)) 584 | } 585 | 586 | // Second turn 587 | do { 588 | messages.append(.user("Now convert those RGB values to hex.")) 589 | 590 | // Second request - convert to hex 591 | response = try await ollama.chat( 592 | model: "llama3.2", 593 | messages: messages, 594 | tools: [hexColorTool] 595 | ) 596 | 597 | // Verify hex tool call 598 | #expect(response.message.toolCalls?.count == 1) 599 | guard let hexCall = response.message.toolCalls?.first else { 600 | Issue.record("Missing hex tool call") 601 | return 602 | } 603 | #expect(hexCall.function.name == "rgb_to_hex") 604 | 605 | // Verify RGB values 606 | guard let red = Double(hexCall.function.arguments["red"]!, strict: false), 607 | let green = Double(hexCall.function.arguments["green"]!), 608 | let blue = Double(hexCall.function.arguments["blue"]!) 609 | else { 610 | Issue.record("Failed to parse RGB values") 611 | return 612 | } 613 | 614 | // Allow for some floating point variance 615 | #expect(abs(red - 1.0) < 0.1) 616 | #expect(abs(green - 0.937) < 0.1) 617 | #expect(abs(blue - 0.835) < 0.1) 618 | } 619 | } 620 | } 621 | -------------------------------------------------------------------------------- /Sources/Ollama/Client.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | /// An Ollama HTTP API client. 8 | /// 9 | /// This client provides methods to interact with the Ollama API, 10 | /// allowing you to generate text, chat, create embeddings, and manage models. 11 | /// 12 | /// - SeeAlso: [Ollama API Documentation](https://github.com/ollama/ollama/blob/main/docs/api.md) 13 | @MainActor 14 | public final class Client: Sendable { 15 | /// The default host URL for the Ollama API. 16 | public static let defaultHost = URL(string: "http://localhost:11434")! 17 | 18 | /// A default client instance using the default host. 19 | public static let `default` = Client(host: defaultHost) 20 | 21 | /// The host URL for requests made by the client. 22 | public let host: URL 23 | 24 | /// The value for the `User-Agent` header sent in requests, if any. 25 | public let userAgent: String? 26 | 27 | /// The underlying client session. 28 | private let session: URLSession 29 | 30 | /// Creates a client with the specified session, host, and user agent. 31 | /// 32 | /// - Parameters: 33 | /// - session: The underlying client session. Defaults to `URLSession(configuration: .default)`. 34 | /// - host: The host URL to use for requests. 35 | /// - userAgent: The value for the `User-Agent` header sent in requests, if any. Defaults to `nil`. 36 | public init( 37 | session: URLSession = URLSession(configuration: .default), 38 | host: URL, 39 | userAgent: String? = nil 40 | ) { 41 | var host = host 42 | if !host.path.hasSuffix("/") { 43 | host = host.appendingPathComponent("") 44 | } 45 | 46 | self.host = host 47 | self.userAgent = userAgent 48 | self.session = session 49 | } 50 | 51 | /// Represents errors that can occur during API operations. 52 | public enum Error: Swift.Error, CustomStringConvertible { 53 | /// An error encountered while constructing the request. 54 | case requestError(String) 55 | 56 | /// An error returned by the Ollama HTTP API. 57 | case responseError(response: HTTPURLResponse, detail: String) 58 | 59 | /// An error encountered while decoding the response. 60 | case decodingError(response: HTTPURLResponse, detail: String) 61 | 62 | /// An unexpected error. 63 | case unexpectedError(String) 64 | 65 | // MARK: CustomStringConvertible 66 | 67 | public var description: String { 68 | switch self { 69 | case .requestError(let detail): 70 | return "Request error: \(detail)" 71 | case .responseError(let response, let detail): 72 | return "Response error (Status \(response.statusCode)): \(detail)" 73 | case .decodingError(let response, let detail): 74 | return "Decoding error (Status \(response.statusCode)): \(detail)" 75 | case .unexpectedError(let detail): 76 | return "Unexpected error: \(detail)" 77 | } 78 | } 79 | } 80 | 81 | private struct ErrorResponse: Decodable { 82 | let error: String 83 | } 84 | 85 | enum Method: String, Hashable { 86 | case get = "GET" 87 | case post = "POST" 88 | case delete = "DELETE" 89 | } 90 | 91 | func fetch( 92 | _ method: Method, 93 | _ path: String, 94 | params: [String: Value]? = nil 95 | ) async throws -> T { 96 | let request = try createRequest(method, path, params: params) 97 | let (data, response) = try await session.data(for: request) 98 | let httpResponse = try validateResponse(response) 99 | 100 | let decoder = JSONDecoder() 101 | decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds 102 | 103 | switch httpResponse.statusCode { 104 | case 200..<300: 105 | if T.self == Bool.self { 106 | // If T is Bool, we return true for successful response 107 | return true as! T 108 | } else if data.isEmpty { 109 | throw Error.responseError(response: httpResponse, detail: "Empty response body") 110 | } else { 111 | do { 112 | return try decoder.decode(T.self, from: data) 113 | } catch { 114 | throw Error.decodingError( 115 | response: httpResponse, 116 | detail: "Error decoding response: \(error.localizedDescription)" 117 | ) 118 | } 119 | } 120 | default: 121 | if T.self == Bool.self { 122 | return false as! T 123 | } 124 | 125 | if let errorDetail = try? JSONDecoder().decode(ErrorResponse.self, from: data) { 126 | throw Error.responseError(response: httpResponse, detail: errorDetail.error) 127 | } 128 | 129 | if let string = String(data: data, encoding: .utf8) { 130 | throw Error.responseError(response: httpResponse, detail: string) 131 | } 132 | 133 | throw Error.responseError(response: httpResponse, detail: "Invalid response") 134 | } 135 | } 136 | 137 | func fetchStream( 138 | _ method: Method, 139 | _ path: String, 140 | params: [String: Value]? = nil 141 | ) -> AsyncThrowingStream { 142 | AsyncThrowingStream { continuation in 143 | let task = Task { 144 | let decoder = JSONDecoder() 145 | decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds 146 | 147 | do { 148 | let request = try createRequest(method, path, params: params) 149 | let (bytes, response) = try await session.bytes(for: request) 150 | let httpResponse = try validateResponse(response) 151 | 152 | guard (200..<300).contains(httpResponse.statusCode) else { 153 | var errorData = Data() 154 | for try await byte in bytes { 155 | errorData.append(byte) 156 | } 157 | 158 | if let errorDetail = try? decoder.decode( 159 | ErrorResponse.self, from: errorData) 160 | { 161 | throw Error.responseError( 162 | response: httpResponse, detail: errorDetail.error) 163 | } 164 | 165 | if let string = String(data: errorData, encoding: .utf8) { 166 | throw Error.responseError(response: httpResponse, detail: string) 167 | } 168 | 169 | throw Error.responseError( 170 | response: httpResponse, detail: "Invalid response") 171 | } 172 | 173 | var buffer = Data() 174 | 175 | for try await byte in bytes { 176 | buffer.append(byte) 177 | 178 | // Look for newline to separate JSON objects 179 | while let newlineIndex = buffer.firstIndex(of: UInt8(ascii: "\n")) { 180 | let chunk = buffer[.. URLRequest { 207 | var urlComponents = URLComponents(url: host, resolvingAgainstBaseURL: true) 208 | urlComponents?.path = path 209 | 210 | var httpBody: Data? = nil 211 | switch method { 212 | case .get: 213 | if let params { 214 | var queryItems: [URLQueryItem] = [] 215 | for (key, value) in params { 216 | queryItems.append(URLQueryItem(name: key, value: value.description)) 217 | } 218 | urlComponents?.queryItems = queryItems 219 | } 220 | case .post, .delete: 221 | if let params { 222 | let encoder = JSONEncoder() 223 | httpBody = try encoder.encode(params) 224 | } 225 | } 226 | 227 | guard let url = urlComponents?.url else { 228 | throw Error.requestError( 229 | #"Unable to construct URL with host "\#(host)" and path "\#(path)""#) 230 | } 231 | var request: URLRequest = URLRequest(url: url) 232 | request.httpMethod = method.rawValue 233 | 234 | request.addValue("application/json", forHTTPHeaderField: "Accept") 235 | if let userAgent { 236 | request.addValue(userAgent, forHTTPHeaderField: "User-Agent") 237 | } 238 | 239 | if let httpBody { 240 | request.httpBody = httpBody 241 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 242 | } 243 | 244 | return request 245 | } 246 | 247 | private func validateResponse(_ response: URLResponse) throws -> HTTPURLResponse { 248 | guard let httpResponse = response as? HTTPURLResponse else { 249 | throw Error.unexpectedError("Response is not HTTPURLResponse") 250 | } 251 | return httpResponse 252 | } 253 | } 254 | 255 | // MARK: - Generate 256 | 257 | extension Client { 258 | public struct GenerateResponse: Hashable, Codable, Sendable { 259 | public let model: Model.ID 260 | public let createdAt: Date 261 | public let response: String 262 | public let done: Bool 263 | public let context: [Int]? 264 | public let thinking: String? 265 | public let totalDuration: TimeInterval? 266 | public let loadDuration: TimeInterval? 267 | public let promptEvalCount: Int? 268 | public let promptEvalDuration: TimeInterval? 269 | public let evalCount: Int? 270 | public let evalDuration: TimeInterval? 271 | 272 | enum CodingKeys: String, CodingKey { 273 | case model 274 | case createdAt = "created_at" 275 | case response 276 | case done 277 | case context 278 | case thinking 279 | case totalDuration = "total_duration" 280 | case loadDuration = "load_duration" 281 | case promptEvalCount = "prompt_eval_count" 282 | case promptEvalDuration = "prompt_eval_duration" 283 | case evalCount = "eval_count" 284 | case evalDuration = "eval_duration" 285 | } 286 | 287 | /// Creates a generate response object. 288 | /// - Parameters: 289 | /// - model: The model used for generation. 290 | /// - createdAt: The date and time the response was created. 291 | /// - response: The generated text response. 292 | /// - done: Whether the generation is complete. 293 | /// - context: The context of the generation. 294 | /// - thinking: The thinking of the generation. 295 | /// - totalDuration: The total duration of the generation. 296 | /// - loadDuration: The load duration of the generation. 297 | /// - promptEvalCount: The prompt evaluation count of the generation. 298 | /// - promptEvalDuration: The prompt evaluation duration of the generation. 299 | /// - evalCount: The evaluation count of the generation. 300 | /// - evalDuration: The evaluation duration of the generation. 301 | public init( 302 | model: Model.ID, 303 | createdAt: Date, 304 | response: String, 305 | done: Bool, 306 | context: [Int]? = nil, 307 | thinking: String? = nil, 308 | totalDuration: TimeInterval? = nil, 309 | loadDuration: TimeInterval? = nil, 310 | promptEvalCount: Int? = nil, 311 | promptEvalDuration: TimeInterval? = nil, 312 | evalCount: Int? = nil, 313 | evalDuration: TimeInterval? = nil 314 | ) { 315 | self.model = model 316 | self.createdAt = createdAt 317 | self.response = response 318 | self.done = done 319 | self.context = context 320 | self.thinking = thinking 321 | self.totalDuration = totalDuration 322 | self.loadDuration = loadDuration 323 | self.promptEvalCount = promptEvalCount 324 | self.promptEvalDuration = promptEvalDuration 325 | self.evalCount = evalCount 326 | self.evalDuration = evalDuration 327 | } 328 | } 329 | 330 | /// Generates a response for a given prompt with a provided model. 331 | /// 332 | /// - Parameters: 333 | /// - model: The name of the model to use for generation. 334 | /// - prompt: The prompt to generate a response for. 335 | /// - images: Optional list of base64-encoded images (for multimodal models). 336 | /// - format: Optional format specification. Can be either a string ("json") or a JSON schema to constrain the model's output. 337 | /// - options: Additional model parameters as specified in the Modelfile documentation. 338 | /// - system: System message to override what is defined in the Modelfile. 339 | /// - template: The prompt template to use (overrides what is defined in the Modelfile). 340 | /// - context: The context parameter returned from a previous request to keep a short conversational memory. 341 | /// - raw: If true, no formatting will be applied to the prompt. 342 | /// - think: If true, the model will think about the response before responding. Requires thinking support from the model. 343 | /// - keepAlive: Controls how long the model will stay loaded into memory following the request. Defaults to `.default` which uses the server's default (typically 5 minutes). 344 | /// - Returns: A `GenerateResponse` containing the generated text and additional information. 345 | /// - Throws: An error if the request fails or the response cannot be decoded. 346 | public func generate( 347 | model: Model.ID, 348 | prompt: String, 349 | images: [Data]? = nil, 350 | format: Value? = nil, 351 | options: [String: Value]? = nil, 352 | system: String? = nil, 353 | template: String? = nil, 354 | context: [Int]? = nil, 355 | raw: Bool = false, 356 | think: Bool? = nil, 357 | keepAlive: KeepAlive = .default 358 | ) async throws -> GenerateResponse { 359 | let params = createGenerateParams( 360 | model: model, 361 | prompt: prompt, 362 | images: images, 363 | format: format, 364 | options: options, 365 | system: system, 366 | template: template, 367 | context: context, 368 | raw: raw, 369 | think: think, 370 | keepAlive: keepAlive, 371 | stream: false 372 | ) 373 | return try await fetch(.post, "/api/generate", params: params) 374 | } 375 | 376 | /// Generates a streaming response for a given prompt with a provided model. 377 | /// 378 | /// - Parameters: 379 | /// - model: The name of the model to use for generation. 380 | /// - prompt: The prompt to generate a response for. 381 | /// - images: Optional list of base64-encoded images (for multimodal models). 382 | /// - format: The format to return a response in. Currently, the only accepted value is "json". 383 | /// - options: Additional model parameters as specified in the Modelfile documentation. 384 | /// - system: System message to override what is defined in the Modelfile. 385 | /// - template: The prompt template to use (overrides what is defined in the Modelfile). 386 | /// - context: The context parameter returned from a previous request to keep a short conversational memory. 387 | /// - raw: If true, no formatting will be applied to the prompt. 388 | /// - think: If true, the model will think about the response before responding. Requires thinking support from the model. 389 | /// - keepAlive: Controls how long the model will stay loaded into memory following the request. Defaults to `.default` which uses the server's default (typically 5 minutes). 390 | /// - Returns: An async throwing stream of `GenerateResponse` objects containing generated text chunks and additional information. 391 | /// - Throws: An error if the request fails or responses cannot be decoded. 392 | public func generateStream( 393 | model: Model.ID, 394 | prompt: String, 395 | images: [Data]? = nil, 396 | format: Value? = nil, 397 | options: [String: Value]? = nil, 398 | system: String? = nil, 399 | template: String? = nil, 400 | context: [Int]? = nil, 401 | raw: Bool = false, 402 | think: Bool? = nil, 403 | keepAlive: KeepAlive = .default 404 | ) -> AsyncThrowingStream { 405 | let params = createGenerateParams( 406 | model: model, 407 | prompt: prompt, 408 | images: images, 409 | format: format, 410 | options: options, 411 | system: system, 412 | template: template, 413 | context: context, 414 | raw: raw, 415 | think: think, 416 | keepAlive: keepAlive, 417 | stream: true 418 | ) 419 | return fetchStream(.post, "/api/generate", params: params) 420 | } 421 | 422 | private func createGenerateParams( 423 | model: Model.ID, 424 | prompt: String, 425 | images: [Data]?, 426 | format: Value?, 427 | options: [String: Value]?, 428 | system: String?, 429 | template: String?, 430 | context: [Int]?, 431 | raw: Bool, 432 | think: Bool?, 433 | keepAlive: KeepAlive, 434 | stream: Bool 435 | ) -> [String: Value] { 436 | var params: [String: Value] = [ 437 | "model": .string(model.rawValue), 438 | "prompt": .string(prompt), 439 | "stream": .bool(stream), 440 | "raw": .bool(raw), 441 | ] 442 | 443 | if let images = images { 444 | params["images"] = .array(images.map { .string($0.base64EncodedString()) }) 445 | } 446 | if let format = format { 447 | params["format"] = format 448 | } 449 | if let options = options { 450 | params["options"] = .object(options) 451 | } 452 | if let system = system { 453 | params["system"] = .string(system) 454 | } 455 | if let template = template { 456 | params["template"] = .string(template) 457 | } 458 | if let context = context { 459 | params["context"] = .array(context.map { .double(Double($0)) }) 460 | } 461 | if let think = think { 462 | params["think"] = .bool(think) 463 | } 464 | if let keepAliveValue = keepAlive.value { 465 | params["keep_alive"] = keepAliveValue 466 | } 467 | 468 | return params 469 | } 470 | } 471 | 472 | // MARK: - Chat 473 | 474 | extension Client { 475 | /// Represents a chat response from the model. 476 | public struct ChatResponse: Hashable, Codable, Sendable { 477 | /// The model used for the chat. 478 | public let model: Model.ID 479 | /// The date and time the response was created. 480 | public let createdAt: Date 481 | /// The message of the chat. 482 | public let message: Chat.Message 483 | /// Whether the chat is complete. 484 | public let done: Bool 485 | 486 | /// The total duration of the chat. 487 | public let totalDuration: TimeInterval? 488 | /// The load duration of the chat. 489 | public let loadDuration: TimeInterval? 490 | /// The prompt evaluation count of the chat. 491 | public let promptEvalCount: Int? 492 | /// The prompt evaluation duration of the chat. 493 | public let promptEvalDuration: TimeInterval? 494 | /// The evaluation count of the chat. 495 | public let evalCount: Int? 496 | /// The evaluation duration of the chat. 497 | public let evalDuration: TimeInterval? 498 | 499 | private enum CodingKeys: String, CodingKey { 500 | case model 501 | case createdAt = "created_at" 502 | case message 503 | case done 504 | case totalDuration = "total_duration" 505 | case loadDuration = "load_duration" 506 | case promptEvalCount = "prompt_eval_count" 507 | case promptEvalDuration = "prompt_eval_duration" 508 | case evalCount = "eval_count" 509 | case evalDuration = "eval_duration" 510 | } 511 | 512 | /// Creates a chat response object. 513 | /// - Parameters: 514 | /// - model: The model used for the chat. 515 | /// - createdAt: The date and time the response was created. 516 | /// - message: The message of the chat. 517 | /// - done: Whether the chat is complete. 518 | /// - totalDuration: The total duration of the chat. 519 | /// - loadDuration: The load duration of the chat. 520 | /// - promptEvalCount: The prompt evaluation count of the chat. 521 | /// - promptEvalDuration: The prompt evaluation duration of the chat. 522 | /// - evalCount: The evaluation count of the chat. 523 | /// - evalDuration: The evaluation duration of the chat. 524 | public init( 525 | model: Model.ID, 526 | createdAt: Date, 527 | message: Chat.Message, 528 | done: Bool, 529 | totalDuration: TimeInterval? = nil, 530 | loadDuration: TimeInterval? = nil, 531 | promptEvalCount: Int? = nil, 532 | promptEvalDuration: TimeInterval? = nil, 533 | evalCount: Int? = nil, 534 | evalDuration: TimeInterval? = nil 535 | ) { 536 | self.model = model 537 | self.createdAt = createdAt 538 | self.message = message 539 | self.done = done 540 | self.totalDuration = totalDuration 541 | self.loadDuration = loadDuration 542 | self.promptEvalCount = promptEvalCount 543 | self.promptEvalDuration = promptEvalDuration 544 | self.evalCount = evalCount 545 | self.evalDuration = evalDuration 546 | } 547 | } 548 | 549 | /// Generates the next message in a chat with a provided model. 550 | /// 551 | /// - Parameters: 552 | /// - model: The name of the model to use for the chat. 553 | /// - messages: The messages of the chat, used to keep a chat memory. 554 | /// - options: Additional model parameters as specified in the Modelfile documentation. 555 | /// - template: The prompt template to use (overrides what is defined in the Modelfile). 556 | /// - format: Optional format specification. Can be either a string ("json") or a JSON schema to constrain the model's output. 557 | /// - tools: Optional array of tools that can be called by the model. 558 | /// - think: If true, the model will think about the response before responding. Requires thinking support from the model. 559 | /// - keepAlive: Controls how long the model will stay loaded into memory following the request. Defaults to `.default` which uses the server's default (typically 5 minutes). 560 | /// - Returns: A `ChatResponse` containing the generated message and additional information. 561 | /// - Throws: An error if the request fails or the response cannot be decoded. 562 | public func chat( 563 | model: Model.ID, 564 | messages: [Chat.Message], 565 | options: [String: Value]? = nil, 566 | template: String? = nil, 567 | format: Value? = nil, 568 | tools: [any ToolProtocol]? = nil, 569 | think: Bool? = nil, 570 | keepAlive: KeepAlive = .default 571 | ) async throws -> ChatResponse { 572 | let params = try createChatParams( 573 | model: model, 574 | messages: messages, 575 | options: options, 576 | template: template, 577 | format: format, 578 | tools: tools, 579 | think: think, 580 | keepAlive: keepAlive, 581 | stream: false 582 | ) 583 | return try await fetch(.post, "/api/chat", params: params) 584 | } 585 | 586 | /// Generates a streaming chat response with a provided model. 587 | /// 588 | /// - Parameters: 589 | /// - model: The name of the model to use for the chat. 590 | /// - messages: The messages of the chat, used to keep a chat memory. 591 | /// - options: Additional model parameters as specified in the Modelfile documentation. 592 | /// - template: The prompt template to use (overrides what is defined in the Modelfile). 593 | /// - format: Optional format specification. Can be either a string ("json") or a JSON schema to constrain the model's output. 594 | /// - tools: Optional array of tools that can be called by the model. 595 | /// - think: If true, the model will think about the response before responding. Requires thinking support from the model. 596 | /// - keepAlive: Controls how long the model will stay loaded into memory following the request. Defaults to `.default` which uses the server's default (typically 5 minutes). 597 | /// - Returns: An async throwing stream of `ChatResponse` objects containing generated message chunks and additional information. 598 | /// - Throws: An error if the request fails or responses cannot be decoded. 599 | public func chatStream( 600 | model: Model.ID, 601 | messages: [Chat.Message], 602 | options: [String: Value]? = nil, 603 | template: String? = nil, 604 | format: Value? = nil, 605 | tools: [any ToolProtocol]? = nil, 606 | think: Bool? = nil, 607 | keepAlive: KeepAlive = .default 608 | ) throws -> AsyncThrowingStream { 609 | let params = try createChatParams( 610 | model: model, 611 | messages: messages, 612 | options: options, 613 | template: template, 614 | format: format, 615 | tools: tools, 616 | think: think, 617 | keepAlive: keepAlive, 618 | stream: true 619 | ) 620 | return fetchStream(.post, "/api/chat", params: params) 621 | } 622 | 623 | private func createChatParams( 624 | model: Model.ID, 625 | messages: [Chat.Message], 626 | options: [String: Value]?, 627 | template: String?, 628 | format: Value?, 629 | tools: [any ToolProtocol]?, 630 | think: Bool?, 631 | keepAlive: KeepAlive, 632 | stream: Bool 633 | ) throws -> [String: Value] { 634 | var params: [String: Value] = [ 635 | "model": .string(model.rawValue), 636 | "messages": try Value(messages), 637 | "stream": .bool(stream), 638 | ] 639 | 640 | if let options { 641 | params["options"] = .object(options) 642 | } 643 | 644 | if let template { 645 | params["template"] = .string(template) 646 | } 647 | 648 | if let format { 649 | params["format"] = format 650 | } 651 | 652 | if let tools { 653 | params["tools"] = .array(try tools.map { try Value($0.schema) }) 654 | } 655 | 656 | if let think = think { 657 | params["think"] = .bool(think) 658 | } 659 | 660 | if let keepAliveValue = keepAlive.value { 661 | params["keep_alive"] = keepAliveValue 662 | } 663 | 664 | return params 665 | } 666 | } 667 | 668 | // MARK: - Embeddings 669 | 670 | extension Client { 671 | public struct EmbedResponse: Decodable, Sendable { 672 | public let model: Model.ID 673 | public let embeddings: Embeddings 674 | public let totalDuration: TimeInterval 675 | public let loadDuration: TimeInterval 676 | public let promptEvalCount: Int 677 | 678 | enum CodingKeys: String, CodingKey { 679 | case model 680 | case embeddings 681 | case totalDuration = "total_duration" 682 | case loadDuration = "load_duration" 683 | case promptEvalCount = "prompt_eval_count" 684 | } 685 | } 686 | 687 | /// Generates embeddings from a model for the given input. 688 | /// 689 | /// - Parameters: 690 | /// - model: The name of the model to use for generating embeddings. 691 | /// - input: The text to generate embeddings for. 692 | /// - truncate: If true, truncates the end of each input to fit within context length. Returns error if false and context length is exceeded. 693 | /// - options: Additional model parameters as specified in the Modelfile documentation. 694 | /// - Returns: An `EmbedResponse` containing the generated embeddings and additional information. 695 | /// - Throws: An error if the request fails or the response cannot be decoded. 696 | public func embed( 697 | model: Model.ID, 698 | input: String, 699 | truncate: Bool = true, 700 | options: [String: Value]? = nil 701 | ) 702 | async throws -> EmbedResponse 703 | { 704 | var params: [String: Value] = [ 705 | "model": .string(model.rawValue), 706 | "input": .string(input), 707 | "truncate": .bool(truncate), 708 | ] 709 | 710 | if let options = options { 711 | params["options"] = .object(options) 712 | } 713 | 714 | return try await fetch(.post, "/api/embed", params: params) 715 | } 716 | 717 | /// Generates embeddings from a model for multiple input texts. 718 | /// 719 | /// This method supports batch processing of multiple texts in a single API call, 720 | /// which is more efficient than making multiple individual calls. 721 | /// 722 | /// - Parameters: 723 | /// - model: The name of the model to use for generating embeddings. 724 | /// - inputs: An array of texts to generate embeddings for. 725 | /// - truncate: If true, truncates the end of each input to fit within context length. Returns error if false and context length is exceeded. 726 | /// - options: Additional model parameters as specified in the Modelfile documentation. 727 | /// - Returns: An `EmbedResponse` containing the generated embeddings for all inputs and additional information. 728 | /// - Throws: An error if the request fails or the response cannot be decoded. 729 | public func embed( 730 | model: Model.ID, 731 | inputs: [String], 732 | truncate: Bool = true, 733 | options: [String: Value]? = nil 734 | ) 735 | async throws -> EmbedResponse 736 | { 737 | var params: [String: Value] = [ 738 | "model": .string(model.rawValue), 739 | "input": .array(inputs.map { .string($0) }), 740 | "truncate": .bool(truncate), 741 | ] 742 | 743 | if let options = options { 744 | params["options"] = .object(options) 745 | } 746 | 747 | return try await fetch(.post, "/api/embed", params: params) 748 | } 749 | } 750 | 751 | // MARK: - List Models 752 | 753 | extension Client { 754 | /// Represents a response containing information about available models. 755 | public struct ListModelsResponse: Codable, Sendable { 756 | /// Represents a model. 757 | public struct Model: Codable, Sendable { 758 | /// The name of the model. 759 | public let name: String 760 | /// The date and time the model was modified. 761 | public let modifiedAt: String 762 | /// The size of the model. 763 | public let size: Int64 764 | /// The digest of the model. 765 | public let digest: String 766 | /// The details of the model. 767 | public let details: Ollama.Model.Details 768 | 769 | private enum CodingKeys: String, CodingKey { 770 | case name 771 | case modifiedAt = "modified_at" 772 | case size 773 | case digest 774 | case details 775 | } 776 | 777 | /// Creates a model object. 778 | /// - Parameters: 779 | /// - name: The name of the model. 780 | /// - modifiedAt: The date and time the model was modified. 781 | /// - size: The size of the model. 782 | /// - digest: The digest of the model. 783 | /// - details: The details of the model. 784 | public init( 785 | name: String, 786 | modifiedAt: String, 787 | size: Int64, 788 | digest: String, 789 | details: Ollama.Model.Details 790 | ) { 791 | self.name = name 792 | self.modifiedAt = modifiedAt 793 | self.size = size 794 | self.digest = digest 795 | self.details = details 796 | } 797 | } 798 | 799 | /// The models in the response. 800 | public let models: [Model] 801 | 802 | /// Creates a list models response object. 803 | /// - Parameters: 804 | /// - models: The models in the response. 805 | public init(models: [Model]) { 806 | self.models = models 807 | } 808 | } 809 | 810 | /// Lists models that are available locally. 811 | /// 812 | /// - Returns: A `ListModelsResponse` containing information about available models. 813 | /// - Throws: An error if the request fails or the response cannot be decoded. 814 | public func listModels() async throws -> ListModelsResponse { 815 | return try await fetch(.get, "/api/tags") 816 | } 817 | } 818 | 819 | // MARK: - List Running Models 820 | 821 | extension Client { 822 | /// Represents a response containing information about running models. 823 | public struct ListRunningModelsResponse: Decodable, Sendable { 824 | /// Represents a running model. 825 | public struct Model: Decodable, Sendable { 826 | /// The name of the model. 827 | public let name: String 828 | /// The model of the running model. 829 | public let model: String 830 | /// The size of the running model. 831 | public let size: Int64 832 | /// The digest of the running model. 833 | public let digest: String 834 | /// The details of the running model. 835 | public let details: Ollama.Model.Details 836 | /// The date and time the running model expires. 837 | public let expiresAt: String 838 | /// The size of the running model in VRAM. 839 | public let sizeVRAM: Int64 840 | 841 | private enum CodingKeys: String, CodingKey { 842 | case name 843 | case model 844 | case size 845 | case digest 846 | case details 847 | case expiresAt = "expires_at" 848 | case sizeVRAM = "size_vram" 849 | } 850 | } 851 | 852 | /// The models in the response. 853 | public let models: [Model] 854 | } 855 | 856 | /// Lists models that are currently loaded into memory. 857 | /// 858 | /// - Returns: A `ListRunningModelsResponse` containing information about running models. 859 | /// - Throws: An error if the request fails or the response cannot be decoded. 860 | public func listRunningModels() async throws -> ListRunningModelsResponse { 861 | return try await fetch(.get, "/api/ps") 862 | } 863 | } 864 | 865 | // MARK: - Create Model 866 | 867 | extension Client { 868 | /// Creates a model from a Modelfile. 869 | /// 870 | /// - Parameters: 871 | /// - name: The name of the model to create. 872 | /// - modelfile: The contents of the Modelfile. 873 | /// - path: The path to the Modelfile. 874 | /// - Returns: `true` if the model was successfully created, otherwise `false`. 875 | /// - Throws: An error if the request fails. 876 | public func createModel( 877 | name: Model.ID, 878 | modelfile: String? = nil, 879 | path: String? = nil 880 | ) 881 | async throws -> Bool 882 | { 883 | var params: [String: Value] = ["name": .string(name.rawValue)] 884 | if let modelfile = modelfile { 885 | params["modelfile"] = .string(modelfile) 886 | } 887 | if let path = path { 888 | params["path"] = .string(path) 889 | } 890 | return try await fetch(.post, "/api/create", params: params) 891 | } 892 | } 893 | 894 | // MARK: - Copy Model 895 | 896 | extension Client { 897 | /// Copies a model. 898 | /// 899 | /// - Parameters: 900 | /// - source: The name of the source model. 901 | /// - destination: The name of the destination model. 902 | /// - Returns: `true` if the model was successfully copied, otherwise `false`. 903 | /// - Throws: An error if the request fails. 904 | public func copyModel(source: String, destination: String) async throws -> Bool { 905 | let params: [String: Value] = [ 906 | "source": .string(source), 907 | "destination": .string(destination), 908 | ] 909 | return try await fetch(.post, "/api/copy", params: params) 910 | } 911 | } 912 | 913 | // MARK: - Delete Model 914 | 915 | extension Client { 916 | /// Deletes a model and its data. 917 | /// 918 | /// - Parameter id: The name of the model to delete. 919 | /// - Returns: `true` if the model was successfully deleted, otherwise `false`. 920 | /// - Throws: An error if the operation fails. 921 | public func deleteModel(_ id: Model.ID) async throws -> Bool { 922 | return try await fetch(.delete, "/api/delete", params: ["name": .string(id.rawValue)]) 923 | } 924 | } 925 | 926 | // MARK: - Pull Model 927 | 928 | extension Client { 929 | /// Downloads a model from the Ollama library. 930 | /// 931 | /// - Parameters: 932 | /// - id: The name of the model to pull. 933 | /// - insecure: If true, allows insecure connections to the library. Only use this if you are pulling from your own library during development. 934 | /// - Returns: `true` if the model was successfully pulled, otherwise `false`. 935 | /// - Throws: An error if the operation fails. 936 | /// 937 | /// - Note: Cancelled pulls are resumed from where they left off, and multiple calls will share the same download progress. 938 | public func pullModel( 939 | _ id: Model.ID, 940 | insecure: Bool = false 941 | ) 942 | async throws -> Bool 943 | { 944 | let params: [String: Value] = [ 945 | "name": .string(id.rawValue), 946 | "insecure": .bool(insecure), 947 | "stream": false, 948 | ] 949 | return try await fetch(.post, "/api/pull", params: params) 950 | } 951 | } 952 | 953 | // MARK: - Push Model 954 | 955 | extension Client { 956 | /// Uploads a model to a model library. 957 | /// 958 | /// - Parameters: 959 | /// - id: The name of the model to push in the form of "namespace/model:tag". 960 | /// - insecure: If true, allows insecure connections to the library. Only use this if you are pushing to your library during development. 961 | /// - Returns: `true` if the model was successfully pushed, otherwise `false`. 962 | /// - Throws: An error if the operation fails. 963 | /// 964 | /// - Note: Requires registering for ollama.ai and adding a public key first. 965 | public func pushModel( 966 | _ id: Model.ID, 967 | insecure: Bool = false 968 | ) 969 | async throws -> Bool 970 | { 971 | let params: [String: Value] = [ 972 | "name": .string(id.rawValue), 973 | "insecure": .bool(insecure), 974 | "stream": false, 975 | ] 976 | return try await fetch(.post, "/api/push", params: params) 977 | } 978 | } 979 | 980 | // MARK: - Show Model 981 | 982 | extension Client { 983 | /// A response containing information about a model. 984 | public struct ShowModelResponse: Codable, Sendable { 985 | /// The contents of the Modelfile for the model. 986 | public let modelfile: String 987 | 988 | /// The model parameters. 989 | public let parameters: String? 990 | 991 | /// The prompt template used by the model. 992 | public let template: String 993 | 994 | /// Detailed information about the model. 995 | public let details: Model.Details 996 | 997 | /// Additional model information. 998 | public let info: [String: Value] 999 | 1000 | /// Model capabilities (e.g., `"completion"`, `"vision"`). 1001 | public let capabilities: Set 1002 | 1003 | private enum CodingKeys: String, CodingKey { 1004 | case modelfile 1005 | case parameters 1006 | case template 1007 | case details 1008 | case info = "model_info" 1009 | case capabilities 1010 | } 1011 | 1012 | /// Creates a show model response object. 1013 | /// - Parameters: 1014 | /// - modelfile: The contents of the Modelfile for the model. 1015 | /// - parameters: The model parameters. 1016 | /// - template: The prompt template used by the model. 1017 | /// - details: Detailed information about the model. 1018 | /// - info: Additional model information. 1019 | /// - capabilities: The capabilities of the model. 1020 | public init( 1021 | modelfile: String, 1022 | parameters: String?, 1023 | template: String, 1024 | details: Model.Details, 1025 | info: [String: Value], 1026 | capabilities: Set 1027 | ) { 1028 | self.modelfile = modelfile 1029 | self.parameters = parameters 1030 | self.template = template 1031 | self.details = details 1032 | self.info = info 1033 | self.capabilities = capabilities 1034 | } 1035 | } 1036 | 1037 | /// Shows information about a model. 1038 | /// 1039 | /// - Parameter id: The identifier of the model to show information for. 1040 | /// - Returns: A `ShowModelResponse` containing details about the model. 1041 | /// - Throws: An error if the request fails or the response cannot be decoded. 1042 | public func showModel(_ id: Model.ID) async throws -> ShowModelResponse { 1043 | let params: [String: Value] = [ 1044 | "name": .string(id.rawValue) 1045 | ] 1046 | 1047 | return try await fetch(.post, "/api/show", params: params) 1048 | } 1049 | } 1050 | 1051 | // MARK: - Get Ollama Version 1052 | extension Client { 1053 | /// A response containing the version of the Ollama server. 1054 | public struct VersionResponse: Decodable, Sendable { 1055 | /// The Ollama version. 1056 | public let version: String 1057 | } 1058 | 1059 | /// Get the running version of the Ollama server. 1060 | /// 1061 | /// - Returns: A `VersionResponse` containing the ollama version. 1062 | /// - Throws: An error if the request fails or the response cannot be decoded. 1063 | public func version() async throws -> VersionResponse { 1064 | return try await fetch(.get, "/api/version") 1065 | } 1066 | } 1067 | --------------------------------------------------------------------------------