@independo/capacitor-inderun 1.1.0-dev.3 → 2.0.0-dev.1
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 +18 -5
- package/README.md +98 -9
- 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 +3 -3
package/Package.swift
CHANGED
|
@@ -16,15 +16,28 @@ let package = Package(
|
|
|
16
16
|
.library(
|
|
17
17
|
name: "IndeRunCapacitor",
|
|
18
18
|
targets: ["IndeRunCapacitorPlugin"]
|
|
19
|
+
),
|
|
20
|
+
// The Capacitor CLI derives a product name from the npm package name
|
|
21
|
+
// (`@independo/capacitor-inderun` -> `IndependoCapacitorInderun`) and writes exactly
|
|
22
|
+
// that into the app's generated CapApp-SPM manifest. Without a product under this
|
|
23
|
+
// name, `cap add ios` produces a project that cannot resolve its dependencies at all.
|
|
24
|
+
// Kept alongside the original name rather than replacing it, so anything already
|
|
25
|
+
// depending on `IndeRunCapacitor` by URL keeps resolving.
|
|
26
|
+
.library(
|
|
27
|
+
name: "IndependoCapacitorInderun",
|
|
28
|
+
targets: ["IndeRunCapacitorPlugin"]
|
|
19
29
|
)
|
|
20
30
|
],
|
|
21
31
|
dependencies: [
|
|
22
32
|
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
33
|
+
// Ranged rather than `exact:` so a consumer that also depends on `inderun`
|
|
34
|
+
// directly can unify the graph. `.upToNextMinor` rather than `from:` because
|
|
35
|
+
// `from:` on a 0.x means `<1.0.0` — it would let SwiftPM float across a minor
|
|
36
|
+
// (where an 0.x SDK's breaking changes live) while package.json and
|
|
37
|
+
// android/build.gradle.kts stay pinned, which is exactly the three-platform
|
|
38
|
+
// drift the versioning policy forbids. Both operators exclude prereleases:
|
|
39
|
+
// tracking a `-dev.N` again requires going back to `exact:`.
|
|
40
|
+
.package(url: "https://github.com/independo-gmbh/inderun.git", .upToNextMinor(from: "0.3.0"))
|
|
28
41
|
],
|
|
29
42
|
targets: [
|
|
30
43
|
.target(
|
package/README.md
CHANGED
|
@@ -52,6 +52,63 @@ itself. On-device execution needs more than the floor: Apple Foundation Models r
|
|
|
52
52
|
Intelligence–capable device on iOS 26+, and ML Kit GenAI requires AICore / Gemini Nano support.
|
|
53
53
|
Availability is checked at runtime and the cloud provider serves the request when it is missing.
|
|
54
54
|
|
|
55
|
+
## Host Project Requirements
|
|
56
|
+
|
|
57
|
+
The IndeRun SDKs this bridge wraps are newer than a freshly generated Capacitor app's
|
|
58
|
+
defaults, so `cap add` alone is not enough. Each of these is a hard requirement — the
|
|
59
|
+
corresponding build failure is named so it is searchable:
|
|
60
|
+
|
|
61
|
+
**Android** (`android/variables.gradle` and `android/build.gradle` in your app):
|
|
62
|
+
|
|
63
|
+
| Setting | Value | Failure if unset |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `compileSdkVersion` | `37` | *"requires libraries and applications that depend on it to compile against version 37 or later"* |
|
|
66
|
+
| `minSdkVersion` | `26` | manifest merger conflict |
|
|
67
|
+
| Android Gradle Plugin | `9.1.0`+ (Gradle 9.x) | *"requires Android Gradle plugin 9.1.0 or higher"* |
|
|
68
|
+
| `org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.10` on the app's `buildscript` classpath | — | *"Module was compiled with an incompatible version of Kotlin … metadata is 2.4.0, expected version is 2.2.0"* |
|
|
69
|
+
|
|
70
|
+
The Kotlin one is the surprising entry: the `inderun-*` artifacts carry Kotlin 2.4.x
|
|
71
|
+
metadata, which the Kotlin plugin AGP brings by default cannot read. The plugin's own build
|
|
72
|
+
hoists a newer KGP for its standalone build, but a consuming app resolves KGP from its own
|
|
73
|
+
buildscript classpath, so the app has to add it too:
|
|
74
|
+
|
|
75
|
+
```groovy
|
|
76
|
+
// android/build.gradle
|
|
77
|
+
buildscript {
|
|
78
|
+
dependencies {
|
|
79
|
+
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.10'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
AGP 9 also rejects `getDefaultProguardFile('proguard-android.txt')`; switch to
|
|
85
|
+
`proguard-android-optimize.txt`.
|
|
86
|
+
|
|
87
|
+
**iOS**: set `IPHONEOS_DEPLOYMENT_TARGET` to `16.0` in your Xcode project **before**
|
|
88
|
+
`npx cap sync ios`. The Capacitor CLI reads that value to generate `CapApp-SPM/Package.swift`,
|
|
89
|
+
so syncing with the default leaves a manifest pinned below this package's floor and the build
|
|
90
|
+
fails with *"requires minimum platform version 16.0 for the iOS platform, but this target
|
|
91
|
+
supports 15.0"*.
|
|
92
|
+
|
|
93
|
+
## Supported IndeRun Version
|
|
94
|
+
|
|
95
|
+
This release tracks **IndeRun 0.3.0** on all three platforms:
|
|
96
|
+
|
|
97
|
+
| Platform | Artifact | Constraint |
|
|
98
|
+
|----------|----------|------------|
|
|
99
|
+
| Web | `@independo/inderun-web`, `@independo/inderun-contracts` | `0.3.0` (exact) |
|
|
100
|
+
| iOS | `inderun` SwiftPM package | `>=0.3.0 <0.4.0` |
|
|
101
|
+
| Android | `app.independo.inderun:inderun-*` | `0.3.0` (exact) |
|
|
102
|
+
|
|
103
|
+
The npm and Gradle pins are exact and all three are bumped together — a partial bump is how the
|
|
104
|
+
platforms drift apart. The SwiftPM constraint is ranged rather than exact so an app that also depends
|
|
105
|
+
on `inderun` directly can still unify its package graph; it stops at the next minor because that is
|
|
106
|
+
where an 0.x SDK's breaking changes live.
|
|
107
|
+
|
|
108
|
+
This package versions **independently** of the IndeRun monorepo under plain semver — the numbers are
|
|
109
|
+
unrelated, and this bridge's own version says nothing about which IndeRun release it wraps. Read that
|
|
110
|
+
off the table above.
|
|
111
|
+
|
|
55
112
|
## Usage
|
|
56
113
|
|
|
57
114
|
```ts
|
|
@@ -122,23 +179,55 @@ things are specific to reaching them through a bridge:
|
|
|
122
179
|
- `createIndeRunCapacitor(options)` — returns a handle that lazily `configure()`s on the first `run()` or `stream()` and memoizes it. Safe to call once at app startup.
|
|
123
180
|
- `run(request)` — Mode 1. Resolves with the canonical IndeRun `TaskResult`.
|
|
124
181
|
- `stream(request)` — Mode 2. Resolves with a `StreamRun` (`handle`, `events`, `cancel`). `events` is single-use.
|
|
182
|
+
- `checkCapabilities()` — every registered provider's static declaration and live availability, without executing a task. Resolves with `ProviderCapabilitySnapshot[]`. Use it for a provider or settings screen; availability changes between calls, so do not cache it across a `run()` or `stream()`.
|
|
125
183
|
- The low-level plugin methods `configure(options)`, `startStream(options)` and `cancelStream(options)` are also exported, along with the listener event names `STREAM_EVENT_NAME` (`"indeRunStreamEvent"`) and `STREAM_ERROR_NAME` (`"indeRunStreamError"`).
|
|
126
184
|
|
|
127
185
|
> The two listener event names are **public contract**. Native emits exactly these, and an
|
|
128
186
|
> app may attach its own listener to them; renaming one is a breaking change.
|
|
129
187
|
|
|
130
|
-
The `IndeRunCapacitorPlugin
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
188
|
+
The `IndeRunCapacitorPlugin` and `ConfigureOptions` contracts — including the `openAI`,
|
|
189
|
+
`systemModel` and `onnx` bootstrap configs and the `allowDirectOpenAIEndpoint` flag — are
|
|
190
|
+
defined and documented in `src/definitions.ts`.
|
|
191
|
+
|
|
192
|
+
`systemModel`, `onnx` and `allowDirectOpenAIEndpoint` are **web-only** and ignored on iOS
|
|
193
|
+
and Android, which register their own on-device provider from `configure()` regardless.
|
|
194
|
+
`systemModel` registers the browser-managed on-device provider (Chrome's Prompt API) and is
|
|
195
|
+
what makes `constraints.privacy = "local_required"` routable in a browser. Both on-device
|
|
196
|
+
web providers are Mode 1 only.
|
|
197
|
+
|
|
198
|
+
`onnx` carries two caveats. It needs the consumer to install the optional
|
|
199
|
+
`@huggingface/transformers` peer dependency — this bridge does not declare it — and to
|
|
200
|
+
supply real model weights, because the web SDK's `runtime` injection seam is a function and
|
|
201
|
+
so cannot cross a JSON bridge hop: only the default Transformers.js runtime is reachable
|
|
202
|
+
through `configure()`, never the fixture runtime the upstream demos use offline. Register it
|
|
203
|
+
only when the weights are there; a provider that cannot load turns a clean routing refusal
|
|
204
|
+
into a provider error.
|
|
205
|
+
|
|
206
|
+
Two asymmetries in the low-level surface, both hidden by the ergonomic API:
|
|
207
|
+
|
|
208
|
+
- `run(request)` passes the request at the options root, while
|
|
209
|
+
`startStream({ streamId, request })` nests it, because that envelope also carries the
|
|
210
|
+
bridge-local correlation id. `stream()` hides this.
|
|
211
|
+
- The plugin method `checkCapabilities()` resolves `{ providers: [...] }` rather than the
|
|
212
|
+
array itself, because a Capacitor plugin method cannot resolve a top-level array on
|
|
213
|
+
either native platform. `IndeRunCapacitor.checkCapabilities()` unwraps it, so app code
|
|
214
|
+
sees the same array the three platform SDKs return.
|
|
215
|
+
|
|
216
|
+
`ProviderCapabilitySnapshot` and the `ProviderDescriptor` /
|
|
217
|
+
`ProviderDynamicCapabilities` it contains are declared in `src/definitions.ts` rather than
|
|
218
|
+
imported, because — unlike `TaskRequest` or `StreamEvent` — they are not generated
|
|
219
|
+
contracts: each platform SDK declares its own copy and there is no schema or validator for
|
|
220
|
+
them upstream. `src/web.ts` returns the web SDK's snapshots into the bridge's type uncast,
|
|
221
|
+
so the shapes staying identical is a compile error rather than a convention. Note
|
|
222
|
+
`capabilities.streamingAvailable` and `cancellationAvailable` are **absent**, not `null`,
|
|
223
|
+
when the runtime has nothing to add: absence means *inherit the static declaration*.
|
|
138
224
|
|
|
139
225
|
## Platform Notes
|
|
140
226
|
|
|
141
|
-
- Web requires
|
|
227
|
+
- Web requires at least one provider to be registered from `configure()`. The web SDK ships an
|
|
228
|
+
OpenAI-compatible provider, a Web ONNX Runtime provider and a browser system-model provider, but
|
|
229
|
+
only the OpenAI-compatible one declares streaming support — so a `local_required` **stream** in a
|
|
230
|
+
browser is refused at routing time, while a `local_required` **run** can be served on-device.
|
|
142
231
|
- iOS always registers the Apple on-device provider and optionally registers OpenAI when configured.
|
|
143
232
|
- Android always registers the ML Kit on-device provider and optionally registers OpenAI when configured.
|
|
144
233
|
- Keep credentials behind `authContextRef`. That keeps a secret out of the request payload and out
|
package/android/build.gradle.kts
CHANGED
|
@@ -10,7 +10,13 @@ buildscript {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
plugins {
|
|
13
|
-
|
|
13
|
+
// No version here on purpose. When a Capacitor app includes this module, AGP is already
|
|
14
|
+
// on the app's classpath and a versioned request fails with "the plugin is already on the
|
|
15
|
+
// classpath with an unknown version, so compatibility cannot be checked" — i.e. the plugin
|
|
16
|
+
// could not be consumed at all. The version for a standalone build of this directory is
|
|
17
|
+
// declared in settings.gradle's pluginManagement instead, which a consuming app's own
|
|
18
|
+
// settings file replaces.
|
|
19
|
+
id("com.android.library")
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
android {
|
|
@@ -37,11 +43,11 @@ dependencies {
|
|
|
37
43
|
testImplementation("com.capacitorjs:core:8.0.0")
|
|
38
44
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
|
39
45
|
implementation("androidx.core:core-ktx:1.17.0")
|
|
40
|
-
implementation("app.independo.inderun:inderun-contracts:0.3.0
|
|
41
|
-
implementation("app.independo.inderun:inderun-core:0.3.0
|
|
42
|
-
implementation("app.independo.inderun:inderun-kotlin:0.3.0
|
|
43
|
-
implementation("app.independo.inderun:inderun-mlkit-providers:0.3.0
|
|
44
|
-
implementation("app.independo.inderun:inderun-openai-providers:0.3.0
|
|
46
|
+
implementation("app.independo.inderun:inderun-contracts:0.3.0")
|
|
47
|
+
implementation("app.independo.inderun:inderun-core:0.3.0")
|
|
48
|
+
implementation("app.independo.inderun:inderun-kotlin:0.3.0")
|
|
49
|
+
implementation("app.independo.inderun:inderun-mlkit-providers:0.3.0")
|
|
50
|
+
implementation("app.independo.inderun:inderun-openai-providers:0.3.0")
|
|
45
51
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
|
|
46
52
|
|
|
47
53
|
testImplementation("junit:junit:4.13.2")
|
package/android/settings.gradle
CHANGED
|
@@ -4,6 +4,13 @@ pluginManagement {
|
|
|
4
4
|
mavenCentral()
|
|
5
5
|
gradlePluginPortal()
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
// Supplies the version build.gradle.kts deliberately omits, for a standalone build of
|
|
9
|
+
// this directory (pnpm test:android / verify:android). A Capacitor app that includes the
|
|
10
|
+
// module uses its own settings file, so AGP comes from the app's classpath there.
|
|
11
|
+
plugins {
|
|
12
|
+
id("com.android.library") version "9.3.1"
|
|
13
|
+
}
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
dependencyResolutionManagement {
|
|
@@ -87,6 +87,41 @@ class IndeRunCapacitorPlugin : Plugin() {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Reports every registered provider's static declaration and live availability without
|
|
92
|
+
* executing a task. Availability changes between calls — a local model can unload,
|
|
93
|
+
* cloud credentials can expire — so callers must not cache it across a run.
|
|
94
|
+
*/
|
|
95
|
+
@PluginMethod
|
|
96
|
+
fun checkCapabilities(call: PluginCall) {
|
|
97
|
+
scope.launch {
|
|
98
|
+
try {
|
|
99
|
+
val registry = configuredRegistry
|
|
100
|
+
?: throw toIndeRunException(IllegalStateException("Capacitor IndeRun has not been configured. Configure providers before calling checkCapabilities()."))
|
|
101
|
+
// IndeRun is stateless; new per call is intentional — registry is cached after configure().
|
|
102
|
+
val snapshots = IndeRun.initialize(context.applicationContext, registry).checkCapabilities()
|
|
103
|
+
call.resolve(IndeRunSerializer.encodeCapabilitySnapshots(snapshots))
|
|
104
|
+
} catch (error: IndeRunException) {
|
|
105
|
+
val contractError = error.toContractError()
|
|
106
|
+
call.reject(
|
|
107
|
+
contractError.message,
|
|
108
|
+
contractError.errorClass.rawValue,
|
|
109
|
+
null,
|
|
110
|
+
runCatching { IndeRunSerializer.encodeError(contractError) }.getOrNull()
|
|
111
|
+
)
|
|
112
|
+
} catch (error: Throwable) {
|
|
113
|
+
val normalized = toIndeRunException(error)
|
|
114
|
+
val contractError = normalized.toContractError()
|
|
115
|
+
call.reject(
|
|
116
|
+
contractError.message,
|
|
117
|
+
contractError.errorClass.rawValue,
|
|
118
|
+
null,
|
|
119
|
+
runCatching { IndeRunSerializer.encodeError(contractError) }.getOrNull()
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
90
125
|
/**
|
|
91
126
|
* Resolves with the run handle. Only validation and route-selection failures reject;
|
|
92
127
|
* a provider failure, a cancellation, or completion all arrive as the single terminal
|
|
@@ -28,6 +28,9 @@ import app.independo.inderun.contracts.TaskRequestTelemetry
|
|
|
28
28
|
import app.independo.inderun.contracts.TaskResult
|
|
29
29
|
import app.independo.inderun.contracts.TaskResultTelemetry
|
|
30
30
|
import app.independo.inderun.contracts.TelemetryLevel
|
|
31
|
+
import app.independo.inderun.core.ProviderCapabilitySnapshot
|
|
32
|
+
import app.independo.inderun.core.ProviderDescriptor
|
|
33
|
+
import app.independo.inderun.core.ProviderDynamicCapabilities
|
|
31
34
|
import com.getcapacitor.JSArray
|
|
32
35
|
import com.getcapacitor.JSObject
|
|
33
36
|
import org.json.JSONArray
|
|
@@ -113,6 +116,100 @@ object IndeRunSerializer {
|
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Wire encoding for `checkCapabilities()`.
|
|
121
|
+
*
|
|
122
|
+
* Note the enums are written with `.name`, which is the opposite of what
|
|
123
|
+
* [encodeOutcome] and [encodePhase] below have to do. Those contract enums are
|
|
124
|
+
* generated without raw values, so their Kotlin constant names are camelCase and the
|
|
125
|
+
* snake_case wire spelling has to be written by hand. `ProviderDescriptor`'s enums are
|
|
126
|
+
* declared with the wire spelling *as* the constant name (`in_process`,
|
|
127
|
+
* `system_service`), so `.name` already is the wire value — and writing them by hand
|
|
128
|
+
* here would be the thing that eventually drifts.
|
|
129
|
+
*
|
|
130
|
+
* The array is wrapped in a `providers` object because a Capacitor `PluginCall` cannot
|
|
131
|
+
* resolve a top-level array. `IndeRunCapacitor` unwraps it on the JS side.
|
|
132
|
+
*/
|
|
133
|
+
fun encodeCapabilitySnapshots(snapshots: List<ProviderCapabilitySnapshot>): JSObject {
|
|
134
|
+
return JSObject().apply {
|
|
135
|
+
put(
|
|
136
|
+
"providers",
|
|
137
|
+
JSArray().apply {
|
|
138
|
+
snapshots.forEach { snapshot ->
|
|
139
|
+
put(
|
|
140
|
+
JSObject().apply {
|
|
141
|
+
put("providerId", snapshot.providerId)
|
|
142
|
+
put("descriptor", encodeProviderDescriptor(snapshot.descriptor))
|
|
143
|
+
put("capabilities", encodeProviderDynamicCapabilities(snapshot.capabilities))
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fun encodeProviderDescriptor(descriptor: ProviderDescriptor): JSObject {
|
|
153
|
+
return JSObject().apply {
|
|
154
|
+
put("id", descriptor.id)
|
|
155
|
+
put("type", descriptor.type.name)
|
|
156
|
+
put("transport", descriptor.transport.name)
|
|
157
|
+
descriptor.streamingStyle?.let { put("streamingStyle", it.name) }
|
|
158
|
+
put(
|
|
159
|
+
"supports",
|
|
160
|
+
JSObject().apply {
|
|
161
|
+
put("run", descriptor.supports.run)
|
|
162
|
+
put("streaming", descriptor.supports.streaming)
|
|
163
|
+
put("realtime", descriptor.supports.realtime)
|
|
164
|
+
put("tools", descriptor.supports.tools)
|
|
165
|
+
put("reasoningEvents", descriptor.supports.reasoningEvents)
|
|
166
|
+
put("structuredOutput", descriptor.supports.structuredOutput)
|
|
167
|
+
put("multimodal", descriptor.supports.multimodal)
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
put("cancel", descriptor.cancel.name)
|
|
171
|
+
put("tasks", JSArray().apply { descriptor.tasks.forEach { put(it) } })
|
|
172
|
+
descriptor.limits?.let { limits ->
|
|
173
|
+
put(
|
|
174
|
+
"limits",
|
|
175
|
+
JSObject().apply {
|
|
176
|
+
limits.maxInputTokens?.let { put("maxInputTokens", it) }
|
|
177
|
+
limits.maxOutputTokens?.let { put("maxOutputTokens", it) }
|
|
178
|
+
limits.maxImageBytes?.let { put("maxImageBytes", it) }
|
|
179
|
+
limits.maxAudioSeconds?.let { put("maxAudioSeconds", it) }
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
descriptor.privacy?.let { privacy ->
|
|
184
|
+
put(
|
|
185
|
+
"privacy",
|
|
186
|
+
JSObject().apply {
|
|
187
|
+
put("dataLeavesDevice", privacy.dataLeavesDevice)
|
|
188
|
+
privacy.regions?.let { regions ->
|
|
189
|
+
put("regions", JSArray().apply { regions.forEach { put(it) } })
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The three nullable flags are **omitted** rather than written as null. Absence is the
|
|
199
|
+
* contract's "inherit the static declaration" state — `descriptor.supports.streaming`
|
|
200
|
+
* for streaming, `descriptor.cancel != none` for cancellation — and a null would be a
|
|
201
|
+
* third state no consumer has.
|
|
202
|
+
*/
|
|
203
|
+
private fun encodeProviderDynamicCapabilities(capabilities: ProviderDynamicCapabilities): JSObject {
|
|
204
|
+
return JSObject().apply {
|
|
205
|
+
put("available", capabilities.available)
|
|
206
|
+
capabilities.reason?.let { put("reason", it) }
|
|
207
|
+
capabilities.streamingAvailable?.let { put("streamingAvailable", it) }
|
|
208
|
+
capabilities.streamingUnavailableReason?.let { put("streamingUnavailableReason", it) }
|
|
209
|
+
capabilities.cancellationAvailable?.let { put("cancellationAvailable", it) }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
116
213
|
fun encodeStreamRunHandle(handle: StreamRunHandle): JSObject {
|
|
117
214
|
return JSObject().apply {
|
|
118
215
|
put("schemaVersion", handle.schemaVersion.rawValue)
|
package/android/src/test/kotlin/app/independo/inderun/capacitor/IndeRunSerializerCapabilitiesTest.kt
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
package app.independo.inderun.capacitor
|
|
2
|
+
|
|
3
|
+
import app.independo.inderun.core.ProviderCapabilitySnapshot
|
|
4
|
+
import app.independo.inderun.core.ProviderDescriptor
|
|
5
|
+
import app.independo.inderun.core.ProviderDynamicCapabilities
|
|
6
|
+
import org.junit.Assert.assertEquals
|
|
7
|
+
import org.junit.Assert.assertFalse
|
|
8
|
+
import org.junit.Assert.assertTrue
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
|
|
11
|
+
class IndeRunSerializerCapabilitiesTest {
|
|
12
|
+
|
|
13
|
+
private fun snapshot(
|
|
14
|
+
providerId: String,
|
|
15
|
+
type: ProviderDescriptor.ProviderType,
|
|
16
|
+
transport: ProviderDescriptor.TransportType,
|
|
17
|
+
streamingStyle: ProviderDescriptor.StreamingStyle? = null,
|
|
18
|
+
streaming: Boolean,
|
|
19
|
+
cancel: ProviderDescriptor.CancelSemantics,
|
|
20
|
+
limits: ProviderDescriptor.ResourceLimits? = null,
|
|
21
|
+
privacy: ProviderDescriptor.PrivacyDescriptor? = null,
|
|
22
|
+
capabilities: ProviderDynamicCapabilities
|
|
23
|
+
): ProviderCapabilitySnapshot = ProviderCapabilitySnapshot(
|
|
24
|
+
providerId = providerId,
|
|
25
|
+
descriptor = ProviderDescriptor(
|
|
26
|
+
id = providerId,
|
|
27
|
+
type = type,
|
|
28
|
+
transport = transport,
|
|
29
|
+
streamingStyle = streamingStyle,
|
|
30
|
+
supports = ProviderDescriptor.SupportsCapabilities(
|
|
31
|
+
run = true,
|
|
32
|
+
streaming = streaming,
|
|
33
|
+
realtime = false,
|
|
34
|
+
tools = false,
|
|
35
|
+
reasoningEvents = false,
|
|
36
|
+
structuredOutput = false,
|
|
37
|
+
multimodal = false
|
|
38
|
+
),
|
|
39
|
+
cancel = cancel,
|
|
40
|
+
tasks = listOf("text_to_text"),
|
|
41
|
+
limits = limits,
|
|
42
|
+
privacy = privacy
|
|
43
|
+
),
|
|
44
|
+
capabilities = capabilities
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
fun `encodeCapabilitySnapshots wraps the snapshots in a providers array`() {
|
|
49
|
+
val encoded = IndeRunSerializer.encodeCapabilitySnapshots(
|
|
50
|
+
listOf(
|
|
51
|
+
snapshot(
|
|
52
|
+
providerId = "openai",
|
|
53
|
+
type = ProviderDescriptor.ProviderType.cloud,
|
|
54
|
+
transport = ProviderDescriptor.TransportType.http,
|
|
55
|
+
streamingStyle = ProviderDescriptor.StreamingStyle.tokens,
|
|
56
|
+
streaming = true,
|
|
57
|
+
cancel = ProviderDescriptor.CancelSemantics.hard,
|
|
58
|
+
limits = ProviderDescriptor.ResourceLimits(
|
|
59
|
+
maxInputTokens = 128_000,
|
|
60
|
+
maxOutputTokens = 4_096
|
|
61
|
+
),
|
|
62
|
+
privacy = ProviderDescriptor.PrivacyDescriptor(
|
|
63
|
+
dataLeavesDevice = true,
|
|
64
|
+
regions = listOf("us")
|
|
65
|
+
),
|
|
66
|
+
capabilities = ProviderDynamicCapabilities(available = true)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
val providers = encoded.getJSONArray("providers")
|
|
72
|
+
assertEquals(1, providers.length())
|
|
73
|
+
|
|
74
|
+
val provider = providers.getJSONObject(0)
|
|
75
|
+
assertEquals("openai", provider.getString("providerId"))
|
|
76
|
+
|
|
77
|
+
val descriptor = provider.getJSONObject("descriptor")
|
|
78
|
+
assertEquals("openai", descriptor.getString("id"))
|
|
79
|
+
assertEquals("cloud", descriptor.getString("type"))
|
|
80
|
+
assertEquals("http", descriptor.getString("transport"))
|
|
81
|
+
assertEquals("tokens", descriptor.getString("streamingStyle"))
|
|
82
|
+
assertEquals("hard", descriptor.getString("cancel"))
|
|
83
|
+
assertEquals(1, descriptor.getJSONArray("tasks").length())
|
|
84
|
+
assertEquals("text_to_text", descriptor.getJSONArray("tasks").getString(0))
|
|
85
|
+
|
|
86
|
+
val supports = descriptor.getJSONObject("supports")
|
|
87
|
+
assertTrue(supports.getBoolean("run"))
|
|
88
|
+
assertTrue(supports.getBoolean("streaming"))
|
|
89
|
+
assertFalse(supports.getBoolean("realtime"))
|
|
90
|
+
assertFalse(supports.getBoolean("tools"))
|
|
91
|
+
assertFalse(supports.getBoolean("reasoningEvents"))
|
|
92
|
+
assertFalse(supports.getBoolean("structuredOutput"))
|
|
93
|
+
assertFalse(supports.getBoolean("multimodal"))
|
|
94
|
+
|
|
95
|
+
val limits = descriptor.getJSONObject("limits")
|
|
96
|
+
assertEquals(128_000, limits.getInt("maxInputTokens"))
|
|
97
|
+
assertEquals(4_096, limits.getInt("maxOutputTokens"))
|
|
98
|
+
assertFalse(limits.has("maxImageBytes"))
|
|
99
|
+
assertFalse(limits.has("maxAudioSeconds"))
|
|
100
|
+
|
|
101
|
+
val privacy = descriptor.getJSONObject("privacy")
|
|
102
|
+
assertTrue(privacy.getBoolean("dataLeavesDevice"))
|
|
103
|
+
assertEquals("us", privacy.getJSONArray("regions").getString(0))
|
|
104
|
+
|
|
105
|
+
assertTrue(provider.getJSONObject("capabilities").getBoolean("available"))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* `in_process` and `system_service` are the two descriptor constants a future upstream
|
|
110
|
+
* rename to camelCase would silently corrupt on the wire, because the TypeScript union
|
|
111
|
+
* in src/definitions.ts spells them out. Asserting the literals is the guard.
|
|
112
|
+
*/
|
|
113
|
+
@Test
|
|
114
|
+
fun `encodeCapabilitySnapshots writes snake case transport constants verbatim`() {
|
|
115
|
+
val encoded = IndeRunSerializer.encodeCapabilitySnapshots(
|
|
116
|
+
listOf(
|
|
117
|
+
snapshot(
|
|
118
|
+
providerId = "local.onnx.genai.android",
|
|
119
|
+
type = ProviderDescriptor.ProviderType.local,
|
|
120
|
+
transport = ProviderDescriptor.TransportType.in_process,
|
|
121
|
+
streaming = false,
|
|
122
|
+
cancel = ProviderDescriptor.CancelSemantics.soft,
|
|
123
|
+
capabilities = ProviderDynamicCapabilities(
|
|
124
|
+
available = false,
|
|
125
|
+
reason = "No model package configured."
|
|
126
|
+
)
|
|
127
|
+
),
|
|
128
|
+
snapshot(
|
|
129
|
+
providerId = "android.mlkit.genai",
|
|
130
|
+
type = ProviderDescriptor.ProviderType.local,
|
|
131
|
+
transport = ProviderDescriptor.TransportType.system_service,
|
|
132
|
+
streamingStyle = ProviderDescriptor.StreamingStyle.chunks,
|
|
133
|
+
streaming = true,
|
|
134
|
+
cancel = ProviderDescriptor.CancelSemantics.soft,
|
|
135
|
+
capabilities = ProviderDynamicCapabilities(available = true)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
val providers = encoded.getJSONArray("providers")
|
|
141
|
+
assertEquals(
|
|
142
|
+
"in_process",
|
|
143
|
+
providers.getJSONObject(0).getJSONObject("descriptor").getString("transport")
|
|
144
|
+
)
|
|
145
|
+
assertEquals(
|
|
146
|
+
"system_service",
|
|
147
|
+
providers.getJSONObject(1).getJSONObject("descriptor").getString("transport")
|
|
148
|
+
)
|
|
149
|
+
assertEquals(
|
|
150
|
+
"chunks",
|
|
151
|
+
providers.getJSONObject(1).getJSONObject("descriptor").getString("streamingStyle")
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Absence is the contract's "inherit the static declaration" state, so the nullable
|
|
157
|
+
* flags must not reach the wire as nulls — a null would be a third state no consumer
|
|
158
|
+
* has. Same for the optional descriptor sub-objects.
|
|
159
|
+
*/
|
|
160
|
+
@Test
|
|
161
|
+
fun `encodeCapabilitySnapshots omits unset optional fields instead of writing null`() {
|
|
162
|
+
val encoded = IndeRunSerializer.encodeCapabilitySnapshots(
|
|
163
|
+
listOf(
|
|
164
|
+
snapshot(
|
|
165
|
+
providerId = "android.mlkit.genai",
|
|
166
|
+
type = ProviderDescriptor.ProviderType.local,
|
|
167
|
+
transport = ProviderDescriptor.TransportType.system_service,
|
|
168
|
+
streaming = true,
|
|
169
|
+
cancel = ProviderDescriptor.CancelSemantics.soft,
|
|
170
|
+
capabilities = ProviderDynamicCapabilities(available = true)
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
val provider = encoded.getJSONArray("providers").getJSONObject(0)
|
|
176
|
+
val descriptor = provider.getJSONObject("descriptor")
|
|
177
|
+
assertFalse(descriptor.has("streamingStyle"))
|
|
178
|
+
assertFalse(descriptor.has("limits"))
|
|
179
|
+
assertFalse(descriptor.has("privacy"))
|
|
180
|
+
|
|
181
|
+
val capabilities = provider.getJSONObject("capabilities")
|
|
182
|
+
assertFalse(capabilities.has("reason"))
|
|
183
|
+
assertFalse(capabilities.has("streamingAvailable"))
|
|
184
|
+
assertFalse(capabilities.has("streamingUnavailableReason"))
|
|
185
|
+
assertFalse(capabilities.has("cancellationAvailable"))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@Test
|
|
189
|
+
fun `encodeCapabilitySnapshots encodes streaming taken away at runtime`() {
|
|
190
|
+
val encoded = IndeRunSerializer.encodeCapabilitySnapshots(
|
|
191
|
+
listOf(
|
|
192
|
+
snapshot(
|
|
193
|
+
providerId = "openai",
|
|
194
|
+
type = ProviderDescriptor.ProviderType.cloud,
|
|
195
|
+
transport = ProviderDescriptor.TransportType.http,
|
|
196
|
+
streamingStyle = ProviderDescriptor.StreamingStyle.tokens,
|
|
197
|
+
streaming = true,
|
|
198
|
+
cancel = ProviderDescriptor.CancelSemantics.hard,
|
|
199
|
+
capabilities = ProviderDynamicCapabilities(
|
|
200
|
+
available = true,
|
|
201
|
+
streamingAvailable = false,
|
|
202
|
+
streamingUnavailableReason = "Host has no streaming HTTP client.",
|
|
203
|
+
cancellationAvailable = true
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
val capabilities = encoded.getJSONArray("providers")
|
|
210
|
+
.getJSONObject(0)
|
|
211
|
+
.getJSONObject("capabilities")
|
|
212
|
+
assertTrue(capabilities.getBoolean("available"))
|
|
213
|
+
assertFalse(capabilities.getBoolean("streamingAvailable"))
|
|
214
|
+
assertEquals(
|
|
215
|
+
"Host has no streaming HTTP client.",
|
|
216
|
+
capabilities.getString("streamingUnavailableReason")
|
|
217
|
+
)
|
|
218
|
+
assertTrue(capabilities.getBoolean("cancellationAvailable"))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Test
|
|
222
|
+
fun `encodeCapabilitySnapshots encodes an empty registry as an empty array`() {
|
|
223
|
+
val encoded = IndeRunSerializer.encodeCapabilitySnapshots(emptyList())
|
|
224
|
+
|
|
225
|
+
assertTrue(encoded.has("providers"))
|
|
226
|
+
assertEquals(0, encoded.getJSONArray("providers").length())
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Every enum constant of every descriptor enum maps to the wire spelling the
|
|
231
|
+
* TypeScript unions declare. Exhaustive by construction: `values()` grows when
|
|
232
|
+
* upstream adds a constant, and a renamed one fails here rather than in a demo.
|
|
233
|
+
*/
|
|
234
|
+
@Test
|
|
235
|
+
fun `every descriptor enum constant maps to its declared wire value`() {
|
|
236
|
+
assertEquals(
|
|
237
|
+
listOf("local", "edge", "cloud"),
|
|
238
|
+
ProviderDescriptor.ProviderType.values().map { it.name }
|
|
239
|
+
)
|
|
240
|
+
assertEquals(
|
|
241
|
+
listOf("in_process", "system_service", "http", "sse", "realtime"),
|
|
242
|
+
ProviderDescriptor.TransportType.values().map { it.name }
|
|
243
|
+
)
|
|
244
|
+
assertEquals(
|
|
245
|
+
listOf("tokens", "chunks", "snapshots"),
|
|
246
|
+
ProviderDescriptor.StreamingStyle.values().map { it.name }
|
|
247
|
+
)
|
|
248
|
+
assertEquals(
|
|
249
|
+
listOf("hard", "soft", "none"),
|
|
250
|
+
ProviderDescriptor.CancelSemantics.values().map { it.name }
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
}
|