@swiftbrowser/native 0.18.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -11
- package/bin/swiftbrowser-native.mjs +47 -0
- package/dist/blob.d.ts +85 -11
- package/dist/blob.d.ts.map +1 -1
- package/dist/blob.js +184 -34
- package/dist/blob.js.map +1 -1
- package/dist/bridge.d.ts +66 -8
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/encoding.d.ts +13 -0
- package/dist/encoding.d.ts.map +1 -1
- package/dist/encoding.js +24 -0
- package/dist/encoding.js.map +1 -1
- package/dist/fetch.d.ts +27 -13
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +117 -45
- package/dist/fetch.js.map +1 -1
- package/dist/hostRequests.d.ts +24 -6
- package/dist/hostRequests.d.ts.map +1 -1
- package/dist/hostRequests.js +108 -15
- package/dist/hostRequests.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/ios.d.ts +79 -0
- package/dist/ios.d.ts.map +1 -0
- package/dist/ios.js +181 -0
- package/dist/ios.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +16 -7
- package/dist/runtime.js.map +1 -1
- package/dist/subtle.d.ts +1 -1
- package/dist/subtle.d.ts.map +1 -1
- package/dist/subtle.js +9 -5
- package/dist/subtle.js.map +1 -1
- package/dist/webSocket.d.ts +12 -5
- package/dist/webSocket.d.ts.map +1 -1
- package/dist/webSocket.js +37 -11
- package/dist/webSocket.js.map +1 -1
- package/package.json +7 -3
- package/src/blob.ts +215 -34
- package/src/bridge.ts +56 -8
- package/src/encoding.ts +22 -0
- package/src/fetch.ts +111 -48
- package/src/hostRequests.ts +129 -22
- package/src/index.ts +1 -2
- package/src/ios.ts +253 -0
- package/src/runtime.ts +23 -8
- package/src/subtle.ts +10 -6
- package/src/webSocket.ts +37 -13
- package/swift/Packages/SwiftBrowserDSP/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserDSP/README.md +42 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +306 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +1561 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +483 -0
- package/swift/Packages/SwiftBrowserLocalModel/Package.swift +35 -0
- package/swift/Packages/SwiftBrowserLocalModel/README.md +66 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LlamaEngine.swift +223 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LocalLanguageModel.swift +409 -0
- package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserShader/README.md +45 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineBridge.swift +921 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineNodes.swift +70 -0
- package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +144 -10
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +151 -27
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +139 -40
- package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +116 -0
- package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +141 -2
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelBridge.swift +511 -0
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelProvider.swift +195 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +24 -11
- package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
- package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +12 -7
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +18 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +41 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +88 -17
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +87 -21
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PinchTracker.swift +200 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +103 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +146 -0
- package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +31 -2
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
- package/swift/Sources/SwiftBrowserTree/OpTree.swift +25 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
- package/dist/native.d.ts +0 -19
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -17
- package/dist/native.js.map +0 -1
- package/src/native.ts +0 -35
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +0 -53
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#if canImport(SwiftUI) && !canImport(FoundationBridge)
|
|
2
|
+
import Foundation
|
|
3
|
+
import SwiftBrowserProtocol
|
|
4
|
+
#if canImport(FoundationModels)
|
|
5
|
+
import FoundationModels
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
/// Foundation Models for a JavaScript app (`LanguageModelSession` in
|
|
9
|
+
/// @swiftbrowser/react; docs/ops-protocol.md, "The on-device language
|
|
10
|
+
/// model"): `languageModel` requests, answered by Apple Intelligence's
|
|
11
|
+
/// on-device model where it can answer (iOS 26 and macOS 26), else by the
|
|
12
|
+
/// app's `LanguageModels.fallback` (LanguageModelProvider.swift), else not at
|
|
13
|
+
/// all. The commands are the page's (Web/src/languageModel.ts):
|
|
14
|
+
///
|
|
15
|
+
/// - `availability` → `{ status: "available" }` or `{ status: "unavailable",
|
|
16
|
+
/// reason }`, the model that would answer, with `download: { size?,
|
|
17
|
+
/// downloaded, progress?, failure? }` where it is a model the app fetches;
|
|
18
|
+
/// and `models`, the choices (Apple Intelligence where it answers, then the
|
|
19
|
+
/// fallback's), each `{ id, name, summary?, selected, download? }`;
|
|
20
|
+
/// - `select` makes the choice `id` answer from the next session on
|
|
21
|
+
/// (remembered across launches);
|
|
22
|
+
/// - `download` starts fetching the app's model, `deleteDownload` removes it;
|
|
23
|
+
/// - `open` makes the app's session `session` with its `instructions` and
|
|
24
|
+
/// `tools` (`[{ name, description, parameters }]`, the arguments a JSON
|
|
25
|
+
/// schema) on that model, `prewarm` loads it, `close` lets it go;
|
|
26
|
+
/// - `toolCall` answers the session's next call of one of its tools, once
|
|
27
|
+
/// the model makes one, as `{ call, name, arguments }` (`{ closed: true }`
|
|
28
|
+
/// once the session is closed); the app runs it and answers with
|
|
29
|
+
/// `toolResult` (`call`, and `output` or `error`), which the model reads;
|
|
30
|
+
/// - `respond` → `{ content }`, the whole response;
|
|
31
|
+
/// - `stream` starts a response as stream `stream`; each `next` answers
|
|
32
|
+
/// `{ content, done }` once the response has grown or ended, the latest
|
|
33
|
+
/// snapshot (a reader slower than the model skips what it missed);
|
|
34
|
+
/// `cancel` stops it.
|
|
35
|
+
///
|
|
36
|
+
/// A failure rejects as a tagged message (`context: …`, `guardrail: …`),
|
|
37
|
+
/// which the app reads back as its `GenerationError` case.
|
|
38
|
+
@MainActor
|
|
39
|
+
final class LanguageModelBridge {
|
|
40
|
+
/// A response under way, read by `next`.
|
|
41
|
+
@MainActor
|
|
42
|
+
private final class Stream {
|
|
43
|
+
var content = ""
|
|
44
|
+
var done = false
|
|
45
|
+
var error: Error?
|
|
46
|
+
/// Whether the app has had the content as it is now.
|
|
47
|
+
var delivered = true
|
|
48
|
+
var waiter: CheckedContinuation<Void, Never>?
|
|
49
|
+
var task: Task<Void, Never>?
|
|
50
|
+
|
|
51
|
+
func wake() {
|
|
52
|
+
waiter?.resume()
|
|
53
|
+
waiter = nil
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private var sessions: [Int: any LanguageModelProviderSession] = [:]
|
|
58
|
+
/// Each session's tool calls, waiting for the app.
|
|
59
|
+
private var relays: [Int: ToolRelay] = [:]
|
|
60
|
+
private var streams: [Int: Stream] = [:]
|
|
61
|
+
/// Apple Intelligence's model, once asked for (iOS 26 and macOS 26).
|
|
62
|
+
private var system: (any LanguageModelProvider)?
|
|
63
|
+
/// The viewer's choice: `system`, `fallback`, or nil for the default
|
|
64
|
+
/// (Apple Intelligence where it answers, else the app's model).
|
|
65
|
+
private static let choiceKey = "SwiftBrowserNative.languageModelChoice"
|
|
66
|
+
static let systemChoice = "system"
|
|
67
|
+
|
|
68
|
+
func handle(_ params: JSONObject) async throws -> JSONValue {
|
|
69
|
+
switch params["action"]?.string {
|
|
70
|
+
case "availability":
|
|
71
|
+
let current = await choice()
|
|
72
|
+
var answer = Self.json(current.availability, download: current.provider?.downloadState)
|
|
73
|
+
let models = await modelsJSON(current)
|
|
74
|
+
if !models.isEmpty, case .object(var object) = answer {
|
|
75
|
+
object["models"] = .array(models)
|
|
76
|
+
answer = .object(object)
|
|
77
|
+
}
|
|
78
|
+
return answer
|
|
79
|
+
case "select":
|
|
80
|
+
guard let id = params["id"]?.string else { throw HostRequestFailure("languageModel: no id") }
|
|
81
|
+
if id == Self.systemChoice {
|
|
82
|
+
UserDefaults.standard.set(Self.systemChoice, forKey: Self.choiceKey)
|
|
83
|
+
} else {
|
|
84
|
+
UserDefaults.standard.set("fallback", forKey: Self.choiceKey)
|
|
85
|
+
LanguageModels.fallback?.select(id)
|
|
86
|
+
}
|
|
87
|
+
return .object([:])
|
|
88
|
+
case "download":
|
|
89
|
+
LanguageModels.fallback?.startDownload()
|
|
90
|
+
return .object([:])
|
|
91
|
+
case "deleteDownload":
|
|
92
|
+
try await LanguageModels.fallback?.deleteDownload()
|
|
93
|
+
return .object([:])
|
|
94
|
+
case "open":
|
|
95
|
+
let id = try Self.int(params, "session")
|
|
96
|
+
let current = await choice()
|
|
97
|
+
guard let provider = current.provider, current.availability == .available else {
|
|
98
|
+
throw LanguageModelError(.unavailable, "the on-device language model is not available")
|
|
99
|
+
}
|
|
100
|
+
let relay = ToolRelay()
|
|
101
|
+
let tools = (params["tools"]?.array ?? []).compactMap { tool -> LanguageModelTool? in
|
|
102
|
+
guard let object = tool.object, let name = object["name"]?.string else { return nil }
|
|
103
|
+
return LanguageModelTool(
|
|
104
|
+
name: name,
|
|
105
|
+
description: object["description"]?.string ?? "",
|
|
106
|
+
parameters: object["parameters"] ?? .object(["type": .string("object"), "properties": .object([:])]),
|
|
107
|
+
call: { arguments in try await relay.call(name: name, arguments: arguments) }
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
sessions[id] = try provider.makeSession(instructions: params["instructions"]?.string, tools: tools)
|
|
111
|
+
relays[id] = relay
|
|
112
|
+
return .object([:])
|
|
113
|
+
case "toolCall":
|
|
114
|
+
let id = try Self.int(params, "session")
|
|
115
|
+
guard let relay = relays[id] else { return .object(["closed": .bool(true)]) }
|
|
116
|
+
return await relay.next()
|
|
117
|
+
case "toolResult":
|
|
118
|
+
let id = try Self.int(params, "session")
|
|
119
|
+
relays[id]?.answer(try Self.int(params, "call"), output: params["output"]?.string, error: params["error"]?.string)
|
|
120
|
+
return .object([:])
|
|
121
|
+
case "prewarm":
|
|
122
|
+
try session(params).prewarm()
|
|
123
|
+
return .object([:])
|
|
124
|
+
case "respond":
|
|
125
|
+
let response = try session(params).streamResponse(to: params["prompt"]?.string ?? "", options: Self.options(params["options"]))
|
|
126
|
+
var content = ""
|
|
127
|
+
for try await snapshot in response {
|
|
128
|
+
content = snapshot
|
|
129
|
+
}
|
|
130
|
+
return .object(["content": .string(content)])
|
|
131
|
+
case "stream":
|
|
132
|
+
let response = try session(params).streamResponse(to: params["prompt"]?.string ?? "", options: Self.options(params["options"]))
|
|
133
|
+
stream(response, id: try Self.int(params, "stream"))
|
|
134
|
+
return .object([:])
|
|
135
|
+
case "next":
|
|
136
|
+
return try await next(Self.int(params, "stream"))
|
|
137
|
+
case "cancel":
|
|
138
|
+
if let stream = try streams[Self.int(params, "stream")], !stream.done {
|
|
139
|
+
stream.task?.cancel()
|
|
140
|
+
finish(stream)
|
|
141
|
+
}
|
|
142
|
+
return .object([:])
|
|
143
|
+
case "close":
|
|
144
|
+
let id = try Self.int(params, "session")
|
|
145
|
+
sessions.removeValue(forKey: id)?.close()
|
|
146
|
+
relays.removeValue(forKey: id)?.close()
|
|
147
|
+
return .object([:])
|
|
148
|
+
case let action:
|
|
149
|
+
throw HostRequestFailure("unsupported: languageModel action \(action ?? "none")")
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Cancels every response and lets every session go (the app is going away).
|
|
154
|
+
func stop() {
|
|
155
|
+
for stream in streams.values {
|
|
156
|
+
stream.task?.cancel()
|
|
157
|
+
finish(stream)
|
|
158
|
+
}
|
|
159
|
+
streams = [:]
|
|
160
|
+
for session in sessions.values { session.close() }
|
|
161
|
+
sessions = [:]
|
|
162
|
+
for relay in relays.values { relay.close() }
|
|
163
|
+
relays = [:]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// The model that answers now and its availability: Apple Intelligence's
|
|
167
|
+
/// where it can answer and the viewer has not chosen the app's model;
|
|
168
|
+
/// otherwise the app's fallback, unless the system model is only still
|
|
169
|
+
/// downloading and the viewer has not chosen; otherwise none, for the
|
|
170
|
+
/// system's reason.
|
|
171
|
+
private func choice() async -> Choice {
|
|
172
|
+
var reason = LanguageModelAvailability.Reason.deviceNotEligible
|
|
173
|
+
let chosen = UserDefaults.standard.string(forKey: Self.choiceKey)
|
|
174
|
+
var systemAnswers = false
|
|
175
|
+
#if canImport(FoundationModels)
|
|
176
|
+
if #available(iOS 26.0, macOS 26.0, *) {
|
|
177
|
+
let provider = system ?? FoundationModelsProvider()
|
|
178
|
+
system = provider
|
|
179
|
+
let availability = await provider.availability()
|
|
180
|
+
systemAnswers = availability == .available
|
|
181
|
+
if systemAnswers, chosen != "fallback" || LanguageModels.fallback == nil {
|
|
182
|
+
return Choice(provider: provider, availability: availability, systemAnswers: true, isFallback: false)
|
|
183
|
+
}
|
|
184
|
+
if case .unavailable(let systemReason) = availability { reason = systemReason }
|
|
185
|
+
}
|
|
186
|
+
#endif
|
|
187
|
+
if let fallback = LanguageModels.fallback, systemAnswers || reason != .modelNotReady || chosen == "fallback" {
|
|
188
|
+
return Choice(provider: fallback, availability: await fallback.availability(), systemAnswers: systemAnswers, isFallback: true)
|
|
189
|
+
}
|
|
190
|
+
return Choice(provider: nil, availability: .unavailable(reason), systemAnswers: systemAnswers, isFallback: false)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private struct Choice {
|
|
194
|
+
let provider: (any LanguageModelProvider)?
|
|
195
|
+
let availability: LanguageModelAvailability
|
|
196
|
+
/// Whether Apple Intelligence can answer, chosen or not.
|
|
197
|
+
let systemAnswers: Bool
|
|
198
|
+
let isFallback: Bool
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// The models the viewer can choose: Apple Intelligence where it answers,
|
|
202
|
+
/// then the fallback's; none when there is no choice to make.
|
|
203
|
+
private func modelsJSON(_ current: Choice) async -> [JSONValue] {
|
|
204
|
+
var models: [JSONValue] = []
|
|
205
|
+
if current.systemAnswers {
|
|
206
|
+
models.append(.object([
|
|
207
|
+
"id": .string(Self.systemChoice),
|
|
208
|
+
"name": .string("Apple Intelligence"),
|
|
209
|
+
"summary": .string("Built into this device"),
|
|
210
|
+
"selected": .bool(!current.isFallback),
|
|
211
|
+
]))
|
|
212
|
+
}
|
|
213
|
+
if let fallback = LanguageModels.fallback {
|
|
214
|
+
let choices = fallback.choices
|
|
215
|
+
for choice in choices {
|
|
216
|
+
var object: JSONObject = [
|
|
217
|
+
"id": .string(choice.id),
|
|
218
|
+
"name": .string(choice.name),
|
|
219
|
+
"selected": .bool(current.isFallback && choice.id == fallback.selectedChoice),
|
|
220
|
+
]
|
|
221
|
+
if let summary = choice.summary { object["summary"] = .string(summary) }
|
|
222
|
+
if let download = choice.download { object["download"] = Self.json(download) }
|
|
223
|
+
models.append(.object(object))
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return models.count > 1 ? models : []
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private func session(_ params: JSONObject) throws -> any LanguageModelProviderSession {
|
|
230
|
+
let id = try Self.int(params, "session")
|
|
231
|
+
guard let session = sessions[id] else { throw HostRequestFailure("languageModel: no session \(id)") }
|
|
232
|
+
return session
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private func stream(_ response: AsyncThrowingStream<String, Error>, id: Int) {
|
|
236
|
+
let stream = Stream()
|
|
237
|
+
streams[id] = stream
|
|
238
|
+
stream.task = Task { @MainActor [weak self] in
|
|
239
|
+
do {
|
|
240
|
+
for try await snapshot in response {
|
|
241
|
+
if stream.done { break }
|
|
242
|
+
stream.content = snapshot
|
|
243
|
+
stream.delivered = false
|
|
244
|
+
stream.wake()
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
if !Task.isCancelled { stream.error = error }
|
|
248
|
+
}
|
|
249
|
+
self?.finish(stream)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private func finish(_ stream: Stream) {
|
|
254
|
+
guard !stream.done else { return }
|
|
255
|
+
stream.done = true
|
|
256
|
+
stream.delivered = false
|
|
257
|
+
stream.wake()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private func next(_ id: Int) async throws -> JSONValue {
|
|
261
|
+
guard let stream = streams[id] else { throw HostRequestFailure("languageModel: no stream \(id)") }
|
|
262
|
+
while stream.delivered {
|
|
263
|
+
await withCheckedContinuation { stream.waiter = $0 }
|
|
264
|
+
}
|
|
265
|
+
stream.delivered = true
|
|
266
|
+
if stream.done { streams[id] = nil }
|
|
267
|
+
if let error = stream.error { throw error }
|
|
268
|
+
return .object(["content": .string(stream.content), "done": .bool(stream.done)])
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// MARK: Conversions
|
|
272
|
+
|
|
273
|
+
static func json(_ availability: LanguageModelAvailability, download: LanguageModelDownload? = nil) -> JSONValue {
|
|
274
|
+
var object: JSONObject
|
|
275
|
+
switch availability {
|
|
276
|
+
case .available:
|
|
277
|
+
object = ["status": .string("available")]
|
|
278
|
+
case .unavailable(let reason):
|
|
279
|
+
object = ["status": .string("unavailable"), "reason": .string(reason.rawValue)]
|
|
280
|
+
}
|
|
281
|
+
if let download { object["download"] = json(download) }
|
|
282
|
+
return .object(object)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
static func json(_ download: LanguageModelDownload) -> JSONValue {
|
|
286
|
+
var state: JSONObject = ["downloaded": .bool(download.isDownloaded)]
|
|
287
|
+
if let size = download.size { state["size"] = .number(Double(size)) }
|
|
288
|
+
if let progress = download.progress { state["progress"] = .number(progress) }
|
|
289
|
+
if let failure = download.failure { state["failure"] = .string(failure) }
|
|
290
|
+
return .object(state)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
static func options(_ value: JSONValue?) -> LanguageModelOptions {
|
|
294
|
+
let options = value?.object ?? [:]
|
|
295
|
+
return LanguageModelOptions(
|
|
296
|
+
temperature: options["temperature"]?.double,
|
|
297
|
+
maximumResponseTokens: options["maximumResponseTokens"]?.int,
|
|
298
|
+
greedy: options["sampling"]?.string == "greedy"
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private static func int(_ params: JSONObject, _ key: String) throws -> Int {
|
|
303
|
+
guard let value = params[key]?.int else { throw HostRequestFailure("languageModel: no \(key)") }
|
|
304
|
+
return value
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// A session's tool calls on their way to the app and back: the model's
|
|
309
|
+
/// call waits here until the app, asking with `toolCall`, has run it and
|
|
310
|
+
/// answered with `toolResult`.
|
|
311
|
+
@MainActor
|
|
312
|
+
final class ToolRelay {
|
|
313
|
+
private struct Call {
|
|
314
|
+
let id: Int
|
|
315
|
+
let name: String
|
|
316
|
+
let arguments: JSONValue
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private var waiting: [Call] = []
|
|
320
|
+
private var answers: [Int: CheckedContinuation<String, Error>] = [:]
|
|
321
|
+
/// The app's `toolCall` request, when it asked before the model called.
|
|
322
|
+
private var asker: CheckedContinuation<JSONValue, Never>?
|
|
323
|
+
private var nextID = 1
|
|
324
|
+
private var closed = false
|
|
325
|
+
|
|
326
|
+
/// Hands the call to the app and waits for what it answers.
|
|
327
|
+
func call(name: String, arguments: JSONValue) async throws -> String {
|
|
328
|
+
guard !closed else { throw LanguageModelError(.unavailable, "the session is closed") }
|
|
329
|
+
let id = nextID
|
|
330
|
+
nextID += 1
|
|
331
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
332
|
+
answers[id] = continuation
|
|
333
|
+
let call = Call(id: id, name: name, arguments: arguments)
|
|
334
|
+
if let asker {
|
|
335
|
+
self.asker = nil
|
|
336
|
+
asker.resume(returning: Self.json(call))
|
|
337
|
+
} else {
|
|
338
|
+
waiting.append(call)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// The next call for the app to run, once the model makes one.
|
|
344
|
+
func next() async -> JSONValue {
|
|
345
|
+
if !waiting.isEmpty { return Self.json(waiting.removeFirst()) }
|
|
346
|
+
if closed { return .object(["closed": .bool(true)]) }
|
|
347
|
+
return await withCheckedContinuation { asker = $0 }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
func answer(_ id: Int, output: String?, error: String?) {
|
|
351
|
+
guard let continuation = answers.removeValue(forKey: id) else { return }
|
|
352
|
+
if let error {
|
|
353
|
+
continuation.resume(throwing: LanguageModelError(.unavailable, "the tool failed: \(error)"))
|
|
354
|
+
} else {
|
|
355
|
+
continuation.resume(returning: output ?? "")
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// The session is gone: the app stops asking, and calls under way fail.
|
|
360
|
+
func close() {
|
|
361
|
+
closed = true
|
|
362
|
+
asker?.resume(returning: .object(["closed": .bool(true)]))
|
|
363
|
+
asker = nil
|
|
364
|
+
for continuation in answers.values { continuation.resume(throwing: LanguageModelError(.unavailable, "the session is closed")) }
|
|
365
|
+
answers = [:]
|
|
366
|
+
waiting = []
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private static func json(_ call: Call) -> JSONValue {
|
|
370
|
+
.object(["call": .number(Double(call.id)), "name": .string(call.name), "arguments": call.arguments])
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#if canImport(FoundationModels)
|
|
375
|
+
/// Apple Intelligence's on-device model as a provider: FoundationModels'
|
|
376
|
+
/// `SystemLanguageModel.default` and `LanguageModelSession`.
|
|
377
|
+
@available(iOS 26.0, macOS 26.0, *)
|
|
378
|
+
@MainActor
|
|
379
|
+
final class FoundationModelsProvider: LanguageModelProvider {
|
|
380
|
+
func availability() async -> LanguageModelAvailability {
|
|
381
|
+
// Off the main thread: the first read can take a while (seconds in the Simulator).
|
|
382
|
+
Self.availability(await Task.detached { SystemLanguageModel.default.availability }.value)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
func makeSession(instructions: String?, tools: [LanguageModelTool]) throws -> any LanguageModelProviderSession {
|
|
386
|
+
let bridged: [any Tool] = try tools.map { try BridgedTool($0) }
|
|
387
|
+
return Session(instructions.map { LanguageModelSession(tools: bridged, instructions: $0) } ?? LanguageModelSession(tools: bridged))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// An app's tool as FoundationModels calls one: its arguments' schema made
|
|
391
|
+
/// at run time from the JSON schema, the arguments handed on as JSON.
|
|
392
|
+
struct BridgedTool: Tool {
|
|
393
|
+
let name: String
|
|
394
|
+
let description: String
|
|
395
|
+
let parameters: GenerationSchema
|
|
396
|
+
let run: @Sendable (JSONValue) async throws -> String
|
|
397
|
+
|
|
398
|
+
init(_ tool: LanguageModelTool) throws {
|
|
399
|
+
name = tool.name
|
|
400
|
+
description = tool.description
|
|
401
|
+
parameters = try GenerationSchema(root: Self.schema(tool.parameters, name: "\(tool.name)Arguments"), dependencies: [])
|
|
402
|
+
run = tool.call
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func call(arguments: GeneratedContent) async throws -> String {
|
|
406
|
+
try await run((try? JSONValue.parse(arguments.jsonString)) ?? .object([:]))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// A JSON schema as a generation schema: objects, strings (enums as
|
|
410
|
+
/// choices), numbers, integers, booleans and arrays.
|
|
411
|
+
static func schema(_ json: JSONValue, name: String) -> DynamicGenerationSchema {
|
|
412
|
+
let object = json.object ?? [:]
|
|
413
|
+
let description = object["description"]?.string
|
|
414
|
+
if let choices = object["enum"]?.array?.compactMap(\.string), !choices.isEmpty {
|
|
415
|
+
return DynamicGenerationSchema(name: name, description: description, anyOf: choices)
|
|
416
|
+
}
|
|
417
|
+
switch object["type"]?.string {
|
|
418
|
+
case "object":
|
|
419
|
+
let required = Set((object["required"]?.array ?? []).compactMap(\.string))
|
|
420
|
+
let properties = (object["properties"]?.object ?? [:]).sorted { $0.key < $1.key }.map { key, value in
|
|
421
|
+
DynamicGenerationSchema.Property(
|
|
422
|
+
name: key,
|
|
423
|
+
description: value.object?["description"]?.string,
|
|
424
|
+
schema: schema(value, name: "\(name)_\(key)"),
|
|
425
|
+
isOptional: !required.contains(key)
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
return DynamicGenerationSchema(name: name, description: description, properties: properties)
|
|
429
|
+
case "array":
|
|
430
|
+
return DynamicGenerationSchema(arrayOf: schema(object["items"] ?? .object([:]), name: "\(name)_item"))
|
|
431
|
+
case "integer":
|
|
432
|
+
return DynamicGenerationSchema(type: Int.self)
|
|
433
|
+
case "number":
|
|
434
|
+
return DynamicGenerationSchema(type: Double.self)
|
|
435
|
+
case "boolean":
|
|
436
|
+
return DynamicGenerationSchema(type: Bool.self)
|
|
437
|
+
default:
|
|
438
|
+
return DynamicGenerationSchema(type: String.self)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
@MainActor
|
|
444
|
+
final class Session: LanguageModelProviderSession {
|
|
445
|
+
private let session: LanguageModelSession
|
|
446
|
+
|
|
447
|
+
init(_ session: LanguageModelSession) {
|
|
448
|
+
self.session = session
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
func prewarm() {
|
|
452
|
+
session.prewarm()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
func streamResponse(to prompt: String, options: LanguageModelOptions) -> AsyncThrowingStream<String, Error> {
|
|
456
|
+
let session = session
|
|
457
|
+
return AsyncThrowingStream { continuation in
|
|
458
|
+
let task = Task { @MainActor in
|
|
459
|
+
do {
|
|
460
|
+
for try await snapshot in session.streamResponse(to: prompt, options: FoundationModelsProvider.options(options)) {
|
|
461
|
+
continuation.yield(snapshot.content)
|
|
462
|
+
}
|
|
463
|
+
continuation.finish()
|
|
464
|
+
} catch {
|
|
465
|
+
continuation.finish(throwing: FoundationModelsProvider.tagged(error))
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
continuation.onTermination = { _ in task.cancel() }
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
func close() {}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
static func availability(_ availability: SystemLanguageModel.Availability) -> LanguageModelAvailability {
|
|
476
|
+
switch availability {
|
|
477
|
+
case .available:
|
|
478
|
+
return .available
|
|
479
|
+
case .unavailable(let reason):
|
|
480
|
+
switch reason {
|
|
481
|
+
case .deviceNotEligible: return .unavailable(.deviceNotEligible)
|
|
482
|
+
case .appleIntelligenceNotEnabled: return .unavailable(.appleIntelligenceNotEnabled)
|
|
483
|
+
case .modelNotReady: return .unavailable(.modelNotReady)
|
|
484
|
+
@unknown default: return .unavailable(.deviceNotEligible)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
static func options(_ options: LanguageModelOptions) -> GenerationOptions {
|
|
490
|
+
GenerationOptions(sampling: options.greedy ? .greedy : nil, temperature: options.temperature, maximumResponseTokens: options.maximumResponseTokens)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/// A `GenerationError` as the error the app reads its case from.
|
|
494
|
+
static func tagged(_ error: Error) -> Error {
|
|
495
|
+
guard let error = error as? LanguageModelSession.GenerationError else { return error }
|
|
496
|
+
let kind: LanguageModelError.Kind
|
|
497
|
+
switch error {
|
|
498
|
+
case .exceededContextWindowSize: kind = .context
|
|
499
|
+
case .assetsUnavailable: kind = .unavailable
|
|
500
|
+
case .guardrailViolation: kind = .guardrail
|
|
501
|
+
case .unsupportedLanguageOrLocale: kind = .language
|
|
502
|
+
case .rateLimited: kind = .rateLimited
|
|
503
|
+
case .concurrentRequests: kind = .concurrent
|
|
504
|
+
case .refusal: kind = .refusal
|
|
505
|
+
default: return HostRequestFailure(error.localizedDescription)
|
|
506
|
+
}
|
|
507
|
+
return LanguageModelError(kind, error.localizedDescription)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
#endif
|
|
511
|
+
#endif
|