@independo/capacitor-inderun 1.1.0-dev.1 → 1.1.0-dev.11
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/Package.swift +19 -6
- package/README.md +126 -23
- package/android/build.gradle.kts +12 -6
- package/android/settings.gradle +7 -0
- package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunCapacitorPlugin.kt +35 -0
- package/android/src/main/kotlin/app/independo/inderun/capacitor/IndeRunSerializer.kt +97 -0
- package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerCapabilitiesTest.kt +253 -0
- package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerTest.kt +79 -0
- package/dist/definitions.d.ts +122 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/web.d.ts +2 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +66 -7
- package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorBridge.swift +60 -0
- package/ios/Sources/IndeRunCapacitorPlugin/IndeRunCapacitorPlugin.swift +25 -4
- package/ios/Tests/IndeRunCapacitorTests/IndeRunCapacitorBridgeTests.swift +234 -0
- package/package.json +21 -9
package/dist/web.js
CHANGED
|
@@ -10,13 +10,27 @@ export class IndeRunWeb extends WebPlugin {
|
|
|
10
10
|
startingStreams = new Set();
|
|
11
11
|
pendingCancels = new Map();
|
|
12
12
|
async configure(options) {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// The web SDK requires at least one provider, not specifically the cloud one. Held
|
|
14
|
+
// here as well as there so the failure is an `Unavailable` contract error rather than
|
|
15
|
+
// a raw SDK throw, which is what the facade's callers expect.
|
|
16
|
+
if (!options?.openAI && !options?.systemModel && !options?.onnx) {
|
|
17
|
+
throw createUnavailable("Capacitor web execution requires at least one provider registration. Configure with openAI, systemModel and/or onnx bootstrap options before calling run(request).").toContractError();
|
|
15
18
|
}
|
|
16
19
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
// The SDK's own option type, not a hand-written copy of it: an upstream rename
|
|
21
|
+
// then fails at the type level instead of silently dropping a field. This is
|
|
22
|
+
// deliberately the *outbound* shape only — `ConfigureOptions` stays a narrower,
|
|
23
|
+
// JSON-serializable subset, because it has to cross the native bridge hop.
|
|
24
|
+
const webOptions = {};
|
|
25
|
+
if (options.openAI !== undefined) {
|
|
26
|
+
webOptions.openAI = compactOpenAIOptions(options.openAI);
|
|
27
|
+
}
|
|
28
|
+
if (options.systemModel !== undefined) {
|
|
29
|
+
webOptions.systemModel = compactSystemModelOptions(options.systemModel);
|
|
30
|
+
}
|
|
31
|
+
if (options.onnx !== undefined) {
|
|
32
|
+
webOptions.onnx = compactOnnxOptions(options.onnx);
|
|
33
|
+
}
|
|
20
34
|
if (options.allowDirectOpenAIEndpoint !== undefined) {
|
|
21
35
|
webOptions.allowDirectOpenAIEndpoint = options.allowDirectOpenAIEndpoint;
|
|
22
36
|
}
|
|
@@ -37,6 +51,20 @@ export class IndeRunWeb extends WebPlugin {
|
|
|
37
51
|
throw toIndeRunException(error).toContractError();
|
|
38
52
|
}
|
|
39
53
|
}
|
|
54
|
+
async checkCapabilities() {
|
|
55
|
+
if (!this.engine) {
|
|
56
|
+
throw createUnavailable("Capacitor IndeRun has not been configured. Configure providers before calling checkCapabilities().").toContractError();
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
// Deliberately uncast: the engine's snapshots are assigned straight into the
|
|
60
|
+
// bridge's own `ProviderCapabilitySnapshot`, so the two declarations staying
|
|
61
|
+
// identical is a compile error rather than a convention. See definitions.ts.
|
|
62
|
+
return { providers: await this.engine.checkCapabilities() };
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw toIndeRunException(error).toContractError();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
40
68
|
/**
|
|
41
69
|
* The web path deliberately round-trips through `notifyListeners` rather than
|
|
42
70
|
* handing the engine's `StreamRun` straight back.
|
|
@@ -101,8 +129,17 @@ export class IndeRunWeb extends WebPlugin {
|
|
|
101
129
|
}
|
|
102
130
|
}
|
|
103
131
|
}
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Assigns only the fields the caller actually set. `exactOptionalPropertyTypes` makes
|
|
134
|
+
* this incremental style mandatory rather than cosmetic: assigning `undefined` to an
|
|
135
|
+
* optional property is an error, and an explicit `undefined` on the wire is not the same
|
|
136
|
+
* thing as an absent key to the provider that reads it.
|
|
137
|
+
*
|
|
138
|
+
* `id`, `healthCheckTimeoutMs` and `healthCheckCacheMs` are deliberately not bridged —
|
|
139
|
+
* all optional, and the bridge has no reason to expose provider-tuning knobs it cannot
|
|
140
|
+
* also offer on native.
|
|
141
|
+
*/
|
|
142
|
+
function compactOpenAIOptions(openAI) {
|
|
106
143
|
const result = {
|
|
107
144
|
model: openAI.model
|
|
108
145
|
};
|
|
@@ -120,3 +157,25 @@ function compactOpenAIOptions(options) {
|
|
|
120
157
|
}
|
|
121
158
|
return result;
|
|
122
159
|
}
|
|
160
|
+
function compactSystemModelOptions(systemModel) {
|
|
161
|
+
const result = {};
|
|
162
|
+
if (systemModel.id !== undefined) {
|
|
163
|
+
result.id = systemModel.id;
|
|
164
|
+
}
|
|
165
|
+
if (systemModel.timeoutMs !== undefined) {
|
|
166
|
+
result.timeoutMs = systemModel.timeoutMs;
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
function compactOnnxOptions(onnx) {
|
|
171
|
+
const result = {
|
|
172
|
+
modelPackage: onnx.modelPackage
|
|
173
|
+
};
|
|
174
|
+
if (onnx.id !== undefined) {
|
|
175
|
+
result.id = onnx.id;
|
|
176
|
+
}
|
|
177
|
+
if (onnx.timeoutMs !== undefined) {
|
|
178
|
+
result.timeoutMs = onnx.timeoutMs;
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
@@ -49,6 +49,22 @@ struct CapacitorCancelStreamOptions: Codable {
|
|
|
49
49
|
let reason: String?
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/// IndeRunCore's `ProviderCapabilitySnapshot` is `Sendable` but not `Codable`, even
|
|
53
|
+
/// though both of its members are. This mirror exists only to get it through
|
|
54
|
+
/// `JSONEncoder`; see the upstream follow-up asking for `Codable` (and a schema) on
|
|
55
|
+
/// the snapshot itself.
|
|
56
|
+
private struct EncodableCapabilitySnapshot: Encodable {
|
|
57
|
+
let providerId: String
|
|
58
|
+
let descriptor: ProviderDescriptor
|
|
59
|
+
let capabilities: ProviderDynamicCapabilities
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// A Capacitor plugin method must resolve an object, never a top-level array, so the
|
|
63
|
+
/// snapshots travel wrapped. `IndeRunCapacitor` unwraps them on the JS side.
|
|
64
|
+
private struct CapabilitiesEnvelope: Encodable {
|
|
65
|
+
let providers: [EncodableCapabilitySnapshot]
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
/// Called with an already-encoded event/error for one run. The plugin turns these
|
|
53
69
|
/// into `notifyListeners` calls; taking them as closures keeps the pump testable
|
|
54
70
|
/// without Capacitor.
|
|
@@ -82,6 +98,16 @@ final class IndeRunCapacitorBridge {
|
|
|
82
98
|
return try encode(result)
|
|
83
99
|
}
|
|
84
100
|
|
|
101
|
+
func checkCapabilities() async throws -> JSObject {
|
|
102
|
+
guard let registry = configuredRegistry, let hostServices = configuredHostServices else {
|
|
103
|
+
throw createUnavailable(message: "Capacitor IndeRun has not been configured. Configure providers before calling checkCapabilities().")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// IndeRun is a stateless coordinator; new per call is intentional — registry is cached above.
|
|
107
|
+
let engine = IndeRun(registry: registry, hostServices: hostServices)
|
|
108
|
+
return try encode(capabilities: await engine.checkCapabilities())
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
func startStream(
|
|
86
112
|
options: JSObject,
|
|
87
113
|
onEvent: @escaping StreamEventSink,
|
|
@@ -173,6 +199,24 @@ final class IndeRunCapacitorBridge {
|
|
|
173
199
|
try encodeObject(handle)
|
|
174
200
|
}
|
|
175
201
|
|
|
202
|
+
/// The descriptor's enums carry explicit raw values (`in_process`, not `inProcess`),
|
|
203
|
+
/// and JSONEncoder omits nil optionals — so an unset `streamingAvailable` arrives as
|
|
204
|
+
/// an absent key rather than a null, which is what lets the JS side read absence as
|
|
205
|
+
/// "inherit `descriptor.supports.streaming`".
|
|
206
|
+
func encode(capabilities: [ProviderCapabilitySnapshot]) throws -> JSObject {
|
|
207
|
+
try encodeObject(
|
|
208
|
+
CapabilitiesEnvelope(
|
|
209
|
+
providers: capabilities.map {
|
|
210
|
+
EncodableCapabilitySnapshot(
|
|
211
|
+
providerId: $0.providerId,
|
|
212
|
+
descriptor: $0.descriptor,
|
|
213
|
+
capabilities: $0.capabilities
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
176
220
|
private func makeRegistry(openAI: OpenAIProviderBootstrapOptions?) throws -> ProviderRegistry {
|
|
177
221
|
let registry = ProviderRegistry()
|
|
178
222
|
try registry.register(AppleFoundationModelsProvider())
|
|
@@ -222,10 +266,26 @@ final class IndeRunCapacitorBridge {
|
|
|
222
266
|
private func encodeObject<T: Encodable>(_ value: T) throws -> JSObject {
|
|
223
267
|
let data = try JSONEncoder().encode(value)
|
|
224
268
|
let object = try JSONSerialization.jsonObject(with: data, options: [])
|
|
269
|
+
|
|
270
|
+
#if canImport(Capacitor)
|
|
271
|
+
// Capacitor's JSObject is [String: any JSValue], and a plain `as?` cast cannot
|
|
272
|
+
// produce one: JSONSerialization hands back NSDictionary/NSArray values, which do
|
|
273
|
+
// not conform to JSValue, so anything with a nested object or array — a TaskResult's
|
|
274
|
+
// `output`, a stream event's `payload`, the providers array — failed the cast and
|
|
275
|
+
// surfaced as "Capacitor bridge failed to encode a JSON object". JSTypes coerces the
|
|
276
|
+
// tree recursively, which is what the cast was standing in for.
|
|
277
|
+
guard let dictionary = object as? [AnyHashable: Any],
|
|
278
|
+
let coerced = JSTypes.coerceDictionaryToJSObject(dictionary) else {
|
|
279
|
+
throw createInternal(message: "Capacitor bridge failed to encode a JSON object.")
|
|
280
|
+
}
|
|
281
|
+
return coerced
|
|
282
|
+
#else
|
|
283
|
+
// Standalone builds alias JSObject to [String: Any], where the cast is exact.
|
|
225
284
|
guard let dictionary = object as? JSObject else {
|
|
226
285
|
throw createInternal(message: "Capacitor bridge failed to encode a JSON object.")
|
|
227
286
|
}
|
|
228
287
|
return dictionary
|
|
288
|
+
#endif
|
|
229
289
|
}
|
|
230
290
|
|
|
231
291
|
func mapAuthMode(_ value: String?) -> OpenAIAuthMode {
|
|
@@ -10,6 +10,7 @@ public final class IndeRunCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
10
10
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
11
11
|
CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise),
|
|
12
12
|
CAPPluginMethod(name: "run", returnType: CAPPluginReturnPromise),
|
|
13
|
+
CAPPluginMethod(name: "checkCapabilities", returnType: CAPPluginReturnPromise),
|
|
13
14
|
CAPPluginMethod(name: "startStream", returnType: CAPPluginReturnPromise),
|
|
14
15
|
CAPPluginMethod(name: "cancelStream", returnType: CAPPluginReturnPromise)
|
|
15
16
|
]
|
|
@@ -25,7 +26,7 @@ public final class IndeRunCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
25
26
|
|
|
26
27
|
@objc func configure(_ call: CAPPluginCall) {
|
|
27
28
|
do {
|
|
28
|
-
try implementation.configure(options: call.
|
|
29
|
+
try implementation.configure(options: call.jsObjectRepresentation)
|
|
29
30
|
call.resolve()
|
|
30
31
|
} catch let error as IndeRunException {
|
|
31
32
|
let contractError = error.toContractError()
|
|
@@ -42,7 +43,7 @@ public final class IndeRunCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
42
43
|
@objc func run(_ call: CAPPluginCall) {
|
|
43
44
|
Task {
|
|
44
45
|
do {
|
|
45
|
-
let result = try await implementation.run(requestObject: call.
|
|
46
|
+
let result = try await implementation.run(requestObject: call.jsObjectRepresentation)
|
|
46
47
|
call.resolve(result)
|
|
47
48
|
} catch let error as IndeRunException {
|
|
48
49
|
let contractError = error.toContractError()
|
|
@@ -57,6 +58,26 @@ public final class IndeRunCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/// Reports every registered provider's static declaration and live availability
|
|
62
|
+
/// without executing a task. Availability changes between calls — a local model can
|
|
63
|
+
/// unload, cloud credentials can expire — so callers must not cache it across a run.
|
|
64
|
+
@objc func checkCapabilities(_ call: CAPPluginCall) {
|
|
65
|
+
Task {
|
|
66
|
+
do {
|
|
67
|
+
call.resolve(try await implementation.checkCapabilities())
|
|
68
|
+
} catch let error as IndeRunException {
|
|
69
|
+
let contractError = error.toContractError()
|
|
70
|
+
let details = try? implementation.encode(error: contractError)
|
|
71
|
+
call.reject(contractError.message, contractError.errorClass.rawValue, error, details)
|
|
72
|
+
} catch {
|
|
73
|
+
let normalized = toIndeRunException(error)
|
|
74
|
+
let contractError = normalized.toContractError()
|
|
75
|
+
let details = try? implementation.encode(error: contractError)
|
|
76
|
+
call.reject(contractError.message, contractError.errorClass.rawValue, normalized, details)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
/// Resolves with the run handle. Only validation and route-selection failures
|
|
61
82
|
/// reject here; a provider failure, a cancellation, or completion all arrive as
|
|
62
83
|
/// the single terminal event on `indeRunStreamEvent`.
|
|
@@ -69,7 +90,7 @@ public final class IndeRunCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
69
90
|
guard let self else { return }
|
|
70
91
|
do {
|
|
71
92
|
let handle = try await self.implementation.startStream(
|
|
72
|
-
options: call.
|
|
93
|
+
options: call.jsObjectRepresentation,
|
|
73
94
|
onEvent: { [weak self] streamId, event in
|
|
74
95
|
self?.notifyListeners(
|
|
75
96
|
"indeRunStreamEvent",
|
|
@@ -103,7 +124,7 @@ public final class IndeRunCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
103
124
|
/// is a no-op by contract, not an error.
|
|
104
125
|
@objc func cancelStream(_ call: CAPPluginCall) {
|
|
105
126
|
do {
|
|
106
|
-
try implementation.cancelStream(options: call.
|
|
127
|
+
try implementation.cancelStream(options: call.jsObjectRepresentation)
|
|
107
128
|
call.resolve()
|
|
108
129
|
} catch let error as IndeRunException {
|
|
109
130
|
let contractError = error.toContractError()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import XCTest
|
|
2
2
|
import Foundation
|
|
3
3
|
import IndeRunContracts
|
|
4
|
+
import IndeRunCore
|
|
4
5
|
@testable import IndeRunCapacitorPlugin
|
|
5
6
|
|
|
6
7
|
final class IndeRunCapacitorBridgeTests: XCTestCase {
|
|
@@ -86,6 +87,22 @@ final class IndeRunCapacitorBridgeTests: XCTestCase {
|
|
|
86
87
|
XCTAssertEqual(options.allowDirectOpenAIEndpoint, true)
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/// `systemModel` and `onnx` are web-only bootstrap keys, like
|
|
91
|
+
/// `allowDirectOpenAIEndpoint`. Native registers its own on-device provider, so it must
|
|
92
|
+
/// ignore them rather than fail to decode the options object that carries them.
|
|
93
|
+
func testIgnoresTheWebOnlyProviderBootstrapKeys() throws {
|
|
94
|
+
let json: [String: Any] = [
|
|
95
|
+
"openAI": ["model": "gpt-5.2", "auth": "none"],
|
|
96
|
+
"systemModel": ["id": "local.system-model.web", "timeoutMs": 30_000],
|
|
97
|
+
"onnx": ["modelPackage": ["id": "demo", "format": "onnx"]]
|
|
98
|
+
]
|
|
99
|
+
let data = try JSONSerialization.data(withJSONObject: json)
|
|
100
|
+
let options = try JSONDecoder().decode(CapacitorRunOptions.self, from: data)
|
|
101
|
+
|
|
102
|
+
XCTAssertEqual(options.openAI?.model, "gpt-5.2")
|
|
103
|
+
XCTAssertEqual(options.openAI?.auth, "none")
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
// MARK: - encode(error:)
|
|
90
107
|
|
|
91
108
|
func testEncodesIndeRunErrorRequiredFieldsOnly() throws {
|
|
@@ -134,6 +151,223 @@ final class IndeRunCapacitorBridgeTests: XCTestCase {
|
|
|
134
151
|
XCTAssertEqual(encoded["runId"] as? String, "run_abc")
|
|
135
152
|
}
|
|
136
153
|
|
|
154
|
+
/// A routing refusal carries its plan diagnostics on `details`, and since inderun 0.3.0
|
|
155
|
+
/// it does so on iOS too, not just on the web SDK. The shape is nested — a
|
|
156
|
+
/// `rejectedProviders` array of objects each carrying its own `reasons` array — and it
|
|
157
|
+
/// crosses as `[String: JSONAny]`. This is the data a provider-refusal UI reads, so a
|
|
158
|
+
/// regression would be invisible until a demo showed an empty table.
|
|
159
|
+
func testEncodesNestedRoutePlanDiagnosticsInDetails() throws {
|
|
160
|
+
let bridge = IndeRunCapacitorBridge()
|
|
161
|
+
let error = IndeRunError(
|
|
162
|
+
details: [
|
|
163
|
+
"failureCode": JSONAny("CapabilityMismatch"),
|
|
164
|
+
"rejectedProviders": JSONAny([
|
|
165
|
+
[
|
|
166
|
+
"providerId": "apple.foundation-models",
|
|
167
|
+
"reasons": [["code": "capability_unavailable", "message": "Apple Intelligence unavailable."]]
|
|
168
|
+
],
|
|
169
|
+
[
|
|
170
|
+
"providerId": "openai",
|
|
171
|
+
"reasons": [["code": "privacy_constraint", "message": "local_required forbids cloud."]]
|
|
172
|
+
]
|
|
173
|
+
])
|
|
174
|
+
],
|
|
175
|
+
errorClass: .CapabilityMismatch,
|
|
176
|
+
message: "No eligible provider can stream this request.",
|
|
177
|
+
providerId: nil,
|
|
178
|
+
retryable: nil,
|
|
179
|
+
retryAfterMs: nil,
|
|
180
|
+
runId: nil,
|
|
181
|
+
schemaVersion: .the10
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
let encoded = try bridge.encode(error: error)
|
|
185
|
+
let details = try XCTUnwrap(encoded["details"] as? [String: Any])
|
|
186
|
+
XCTAssertEqual(details["failureCode"] as? String, "CapabilityMismatch")
|
|
187
|
+
|
|
188
|
+
let rejected = try XCTUnwrap(details["rejectedProviders"] as? [[String: Any]])
|
|
189
|
+
XCTAssertEqual(rejected.count, 2)
|
|
190
|
+
XCTAssertEqual(rejected[0]["providerId"] as? String, "apple.foundation-models")
|
|
191
|
+
|
|
192
|
+
let reasons = try XCTUnwrap(rejected[0]["reasons"] as? [[String: Any]])
|
|
193
|
+
XCTAssertEqual(reasons[0]["code"] as? String, "capability_unavailable")
|
|
194
|
+
XCTAssertEqual(
|
|
195
|
+
(try XCTUnwrap(rejected[1]["reasons"] as? [[String: Any]]))[0]["code"] as? String,
|
|
196
|
+
"privacy_constraint"
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// MARK: - encode(capabilities:)
|
|
201
|
+
|
|
202
|
+
private func makeSnapshot(
|
|
203
|
+
providerId: String,
|
|
204
|
+
type: ProviderDescriptor.ProviderType,
|
|
205
|
+
transport: ProviderDescriptor.TransportType,
|
|
206
|
+
streamingStyle: ProviderDescriptor.StreamingStyle? = nil,
|
|
207
|
+
streaming: Bool,
|
|
208
|
+
cancel: ProviderDescriptor.CancelSemantics,
|
|
209
|
+
limits: ProviderDescriptor.ResourceLimits? = nil,
|
|
210
|
+
privacy: ProviderDescriptor.PrivacyDescriptor? = nil,
|
|
211
|
+
capabilities: ProviderDynamicCapabilities
|
|
212
|
+
) -> ProviderCapabilitySnapshot {
|
|
213
|
+
ProviderCapabilitySnapshot(
|
|
214
|
+
providerId: providerId,
|
|
215
|
+
descriptor: ProviderDescriptor(
|
|
216
|
+
id: providerId,
|
|
217
|
+
type: type,
|
|
218
|
+
transport: transport,
|
|
219
|
+
streamingStyle: streamingStyle,
|
|
220
|
+
supports: ProviderDescriptor.SupportsCapabilities(
|
|
221
|
+
run: true,
|
|
222
|
+
streaming: streaming,
|
|
223
|
+
realtime: false,
|
|
224
|
+
tools: false,
|
|
225
|
+
reasoningEvents: false,
|
|
226
|
+
structuredOutput: false,
|
|
227
|
+
multimodal: false
|
|
228
|
+
),
|
|
229
|
+
cancel: cancel,
|
|
230
|
+
tasks: ["text_to_text"],
|
|
231
|
+
limits: limits,
|
|
232
|
+
privacy: privacy
|
|
233
|
+
),
|
|
234
|
+
capabilities: capabilities
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func testEncodesCapabilitySnapshotsInsideTheProvidersEnvelope() throws {
|
|
239
|
+
let bridge = IndeRunCapacitorBridge()
|
|
240
|
+
let encoded = try bridge.encode(capabilities: [
|
|
241
|
+
makeSnapshot(
|
|
242
|
+
providerId: "openai",
|
|
243
|
+
type: .cloud,
|
|
244
|
+
transport: .http,
|
|
245
|
+
streamingStyle: .tokens,
|
|
246
|
+
streaming: true,
|
|
247
|
+
cancel: .hard,
|
|
248
|
+
limits: ProviderDescriptor.ResourceLimits(maxInputTokens: 128_000, maxOutputTokens: 4_096),
|
|
249
|
+
privacy: ProviderDescriptor.PrivacyDescriptor(dataLeavesDevice: true, regions: ["us"]),
|
|
250
|
+
capabilities: ProviderDynamicCapabilities(available: true)
|
|
251
|
+
)
|
|
252
|
+
])
|
|
253
|
+
|
|
254
|
+
let providers = try XCTUnwrap(encoded["providers"] as? [[String: Any]])
|
|
255
|
+
XCTAssertEqual(providers.count, 1)
|
|
256
|
+
XCTAssertEqual(providers[0]["providerId"] as? String, "openai")
|
|
257
|
+
|
|
258
|
+
let descriptor = try XCTUnwrap(providers[0]["descriptor"] as? [String: Any])
|
|
259
|
+
XCTAssertEqual(descriptor["id"] as? String, "openai")
|
|
260
|
+
XCTAssertEqual(descriptor["type"] as? String, "cloud")
|
|
261
|
+
XCTAssertEqual(descriptor["transport"] as? String, "http")
|
|
262
|
+
XCTAssertEqual(descriptor["streamingStyle"] as? String, "tokens")
|
|
263
|
+
XCTAssertEqual(descriptor["cancel"] as? String, "hard")
|
|
264
|
+
XCTAssertEqual(descriptor["tasks"] as? [String], ["text_to_text"])
|
|
265
|
+
|
|
266
|
+
let supports = try XCTUnwrap(descriptor["supports"] as? [String: Any])
|
|
267
|
+
XCTAssertEqual(supports["run"] as? Bool, true)
|
|
268
|
+
XCTAssertEqual(supports["streaming"] as? Bool, true)
|
|
269
|
+
XCTAssertEqual(supports["multimodal"] as? Bool, false)
|
|
270
|
+
|
|
271
|
+
let limits = try XCTUnwrap(descriptor["limits"] as? [String: Any])
|
|
272
|
+
XCTAssertEqual(limits["maxInputTokens"] as? Int, 128_000)
|
|
273
|
+
XCTAssertNil(limits["maxImageBytes"])
|
|
274
|
+
|
|
275
|
+
let privacy = try XCTUnwrap(descriptor["privacy"] as? [String: Any])
|
|
276
|
+
XCTAssertEqual(privacy["dataLeavesDevice"] as? Bool, true)
|
|
277
|
+
XCTAssertEqual(privacy["regions"] as? [String], ["us"])
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// `in_process` is the one descriptor constant a future upstream rename to Swift
|
|
281
|
+
/// casing would silently corrupt on the wire, since the JS union spells it out.
|
|
282
|
+
func testEncodesTheInProcessTransportWithItsWireSpelling() throws {
|
|
283
|
+
let bridge = IndeRunCapacitorBridge()
|
|
284
|
+
let encoded = try bridge.encode(capabilities: [
|
|
285
|
+
makeSnapshot(
|
|
286
|
+
providerId: "local.onnx.genai.apple",
|
|
287
|
+
type: .local,
|
|
288
|
+
transport: .inProcess,
|
|
289
|
+
streaming: false,
|
|
290
|
+
cancel: .soft,
|
|
291
|
+
capabilities: ProviderDynamicCapabilities(available: false, reason: "No model package.")
|
|
292
|
+
)
|
|
293
|
+
])
|
|
294
|
+
|
|
295
|
+
let providers = try XCTUnwrap(encoded["providers"] as? [[String: Any]])
|
|
296
|
+
let descriptor = try XCTUnwrap(providers[0]["descriptor"] as? [String: Any])
|
|
297
|
+
XCTAssertEqual(descriptor["transport"] as? String, "in_process")
|
|
298
|
+
XCTAssertNil(descriptor["streamingStyle"])
|
|
299
|
+
|
|
300
|
+
let capabilities = try XCTUnwrap(providers[0]["capabilities"] as? [String: Any])
|
|
301
|
+
XCTAssertEqual(capabilities["available"] as? Bool, false)
|
|
302
|
+
XCTAssertEqual(capabilities["reason"] as? String, "No model package.")
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// Absence means "inherit the static declaration". A null would be a third state
|
|
306
|
+
/// consumers do not have, so the unset optionals must not reach the wire at all.
|
|
307
|
+
func testOmitsUnsetDynamicCapabilityFlagsRatherThanEncodingNull() throws {
|
|
308
|
+
let bridge = IndeRunCapacitorBridge()
|
|
309
|
+
let encoded = try bridge.encode(capabilities: [
|
|
310
|
+
makeSnapshot(
|
|
311
|
+
providerId: "apple.foundation-models",
|
|
312
|
+
type: .local,
|
|
313
|
+
transport: .systemService,
|
|
314
|
+
streamingStyle: .snapshots,
|
|
315
|
+
streaming: true,
|
|
316
|
+
cancel: .soft,
|
|
317
|
+
capabilities: ProviderDynamicCapabilities(available: true)
|
|
318
|
+
)
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
let providers = try XCTUnwrap(encoded["providers"] as? [[String: Any]])
|
|
322
|
+
let descriptor = try XCTUnwrap(providers[0]["descriptor"] as? [String: Any])
|
|
323
|
+
XCTAssertEqual(descriptor["transport"] as? String, "system_service")
|
|
324
|
+
XCTAssertEqual(descriptor["streamingStyle"] as? String, "snapshots")
|
|
325
|
+
XCTAssertNil(descriptor["limits"])
|
|
326
|
+
XCTAssertNil(descriptor["privacy"])
|
|
327
|
+
|
|
328
|
+
let capabilities = try XCTUnwrap(providers[0]["capabilities"] as? [String: Any])
|
|
329
|
+
XCTAssertNil(capabilities["streamingAvailable"])
|
|
330
|
+
XCTAssertNil(capabilities["streamingUnavailableReason"])
|
|
331
|
+
XCTAssertNil(capabilities["cancellationAvailable"])
|
|
332
|
+
XCTAssertNil(capabilities["reason"])
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func testEncodesStreamingTakenAwayAtRuntime() throws {
|
|
336
|
+
let bridge = IndeRunCapacitorBridge()
|
|
337
|
+
let encoded = try bridge.encode(capabilities: [
|
|
338
|
+
makeSnapshot(
|
|
339
|
+
providerId: "openai",
|
|
340
|
+
type: .cloud,
|
|
341
|
+
transport: .http,
|
|
342
|
+
streamingStyle: .tokens,
|
|
343
|
+
streaming: true,
|
|
344
|
+
cancel: .hard,
|
|
345
|
+
capabilities: ProviderDynamicCapabilities(
|
|
346
|
+
available: true,
|
|
347
|
+
streamingAvailable: false,
|
|
348
|
+
streamingUnavailableReason: "Host has no streaming HTTP client.",
|
|
349
|
+
cancellationAvailable: true
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
])
|
|
353
|
+
|
|
354
|
+
let providers = try XCTUnwrap(encoded["providers"] as? [[String: Any]])
|
|
355
|
+
let capabilities = try XCTUnwrap(providers[0]["capabilities"] as? [String: Any])
|
|
356
|
+
XCTAssertEqual(capabilities["streamingAvailable"] as? Bool, false)
|
|
357
|
+
XCTAssertEqual(
|
|
358
|
+
capabilities["streamingUnavailableReason"] as? String,
|
|
359
|
+
"Host has no streaming HTTP client."
|
|
360
|
+
)
|
|
361
|
+
XCTAssertEqual(capabilities["cancellationAvailable"] as? Bool, true)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
func testEncodesAnEmptyRegistryAsAnEmptyProvidersArray() throws {
|
|
365
|
+
let bridge = IndeRunCapacitorBridge()
|
|
366
|
+
let encoded = try bridge.encode(capabilities: [])
|
|
367
|
+
|
|
368
|
+
XCTAssertEqual(try XCTUnwrap(encoded["providers"] as? [[String: Any]]).count, 0)
|
|
369
|
+
}
|
|
370
|
+
|
|
137
371
|
// MARK: - mapAuthMode
|
|
138
372
|
|
|
139
373
|
func testMapAuthModeNoneString() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@independo/capacitor-inderun",
|
|
3
|
-
"version": "1.1.0-dev.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0-dev.11",
|
|
4
|
+
"description": "On-device AI with cloud fallback for Capacitor apps. Apple Foundation Models on iOS, ML Kit GenAI (Gemini Nano) on Android, an OpenAI-compatible endpoint as fallback — one API, with streaming and cancellation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/independo-gmbh/capacitor-inderun#readme",
|
|
7
7
|
"repository": {
|
|
@@ -13,9 +13,21 @@
|
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"capacitor",
|
|
16
|
-
"plugin",
|
|
16
|
+
"capacitor-plugin",
|
|
17
|
+
"ionic",
|
|
17
18
|
"inderun",
|
|
18
|
-
"ai"
|
|
19
|
+
"ai",
|
|
20
|
+
"llm",
|
|
21
|
+
"on-device",
|
|
22
|
+
"on-device-ai",
|
|
23
|
+
"local-llm",
|
|
24
|
+
"offline",
|
|
25
|
+
"cloud-fallback",
|
|
26
|
+
"streaming",
|
|
27
|
+
"openai",
|
|
28
|
+
"gemini-nano",
|
|
29
|
+
"ml-kit",
|
|
30
|
+
"foundation-models"
|
|
19
31
|
],
|
|
20
32
|
"engines": {
|
|
21
33
|
"node": ">=24"
|
|
@@ -58,18 +70,18 @@
|
|
|
58
70
|
},
|
|
59
71
|
"dependencies": {
|
|
60
72
|
"@capacitor/core": "^8.0.0",
|
|
61
|
-
"@independo/inderun-contracts": "0.3.0
|
|
62
|
-
"@independo/inderun-web": "0.3.0
|
|
73
|
+
"@independo/inderun-contracts": "0.3.0",
|
|
74
|
+
"@independo/inderun-web": "0.3.0"
|
|
63
75
|
},
|
|
64
76
|
"devDependencies": {
|
|
65
77
|
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
|
66
|
-
"@semantic-release/changelog": "^
|
|
67
|
-
"@semantic-release/git": "^
|
|
78
|
+
"@semantic-release/changelog": "^7.0.0",
|
|
79
|
+
"@semantic-release/git": "^11.0.1",
|
|
68
80
|
"@types/node": "^24.13.2",
|
|
69
81
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
70
82
|
"semantic-release": "^25.0.5",
|
|
71
83
|
"typescript": "^7.0.2",
|
|
72
|
-
"vitest": "^
|
|
84
|
+
"vitest": "^5.0.2"
|
|
73
85
|
},
|
|
74
86
|
"peerDependencies": {
|
|
75
87
|
"@capacitor/core": "^8.0.0"
|