@ocno/oryn-sdk 0.0.0-stage → 0.1.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/CONTRIBUTING.md +3 -0
- package/LICENSE +21 -0
- package/README.md +184 -2
- package/SECURITY.md +3 -0
- package/dist/index.d.ts +800 -0
- package/dist/index.js +1773 -0
- package/docs/api-reference.md +280 -0
- package/docs/images/request-lifecycle.png +0 -0
- package/docs/images/request-lifecycle.svg +59 -0
- package/docs/recipes.md +288 -0
- package/package.json +34 -3
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# SDK API reference
|
|
2
|
+
|
|
3
|
+
[SDK overview](../README.md) · [Recipes](recipes.md)
|
|
4
|
+
|
|
5
|
+
This reference describes the exported website client in `@ocno/oryn-sdk` 0.1.0. Runtime and model availability are determined by Oryn Browser, independently of the installed npm package.
|
|
6
|
+
|
|
7
|
+
## Client and capabilities
|
|
8
|
+
|
|
9
|
+
### `createLocalAIClient`
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
createLocalAIClient(options?) → LocalAIClient
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Creates a lightweight wrapper. Importing the module and creating a client are safe during SSR; they do not start inference, request permission, or download a model.
|
|
16
|
+
|
|
17
|
+
| Option | Type | Default | Behavior |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| `showOrynBrowserAlert` | `boolean` | `false` | Shows a website-owned explanation once per document outside Oryn; does not grant permission |
|
|
20
|
+
|
|
21
|
+
The client exposes `sdkVersion`, `getCapabilities`, `models`, `artifacts`, `invoke`, `cancel`, and `subscribe`. `SDK_VERSION` is also exported.
|
|
22
|
+
|
|
23
|
+
### `client.getCapabilities`
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
client.getCapabilities() → Promise<RuntimeCapabilities>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Field | Meaning |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `runtimeAvailable` | Whether Local AI is available now |
|
|
32
|
+
| `runtimeState` | `ready`, `starting`, `unavailable`, or `error` |
|
|
33
|
+
| `sdkVersion` | SDK version reported by the capability surface |
|
|
34
|
+
| `browserApiVersion?` | Version of the injected browser contract |
|
|
35
|
+
| `supportedModelProviders` | Supported model-reference providers; the current type allows `huggingface` |
|
|
36
|
+
| `features` | Feature flags listed below |
|
|
37
|
+
| `scheduler?` | Generation-lane scope and maximum concurrent running jobs |
|
|
38
|
+
|
|
39
|
+
The flags are `modelRequests`, `modelSubstitution`, `streaming`, `artifacts`, `artifactUrls`, `cancellation`, `queueing`, and `multipleSubmittedJobs`. Newer peers may also report `modelSelection` and `taskProfiles`; treat absent optional flags as unavailable.
|
|
40
|
+
|
|
41
|
+
Outside Oryn, this method returns unavailable capabilities rather than throwing solely because the bridge is absent. It is not a list of installed models or an exact hardware report.
|
|
42
|
+
|
|
43
|
+
## Models and sessions
|
|
44
|
+
|
|
45
|
+
### `client.models.request`
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
client.models.request(request, options?) → Promise<ModelSession>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A request has a `role` identifying the site's use case, such as `chat` or `transcription`, and one of three forms:
|
|
52
|
+
|
|
53
|
+
| Form | Required data | Purpose |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| Capability request | `requirements.task` and `requirements.outputModalities`; no `preferredModel` | Ask Oryn to resolve an executable match |
|
|
56
|
+
| Explicit request | `preferredModel`; optional requirements and `certifiedModels` | Express a model preference and tested alternatives |
|
|
57
|
+
| Source-checkpoint request | `selection: { version: 1, intent: "source-checkpoint", sourceCheckpoint }` | Let Oryn select an eligible representation of a checkpoint |
|
|
58
|
+
|
|
59
|
+
`ModelRef` has `provider: "huggingface"`, `repository: "owner/name"`, optional `revision`, and optional `files`. File pins require an immutable revision; they are model-host relative file names, not local paths. `ResolvedModelRef` includes the resolved revision and `sourceUrl`.
|
|
60
|
+
|
|
61
|
+
#### Requirements
|
|
62
|
+
|
|
63
|
+
| Field | Purpose |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `task` | Model task, for example `text-generation` or `automatic-speech-recognition` |
|
|
66
|
+
| `inputModalities`, `outputModalities` | Required input/output kinds |
|
|
67
|
+
| `capabilities` | Behavioral requirements such as `chat`, `streaming`, or `speechRecognition` |
|
|
68
|
+
| `contract` | Versioned task reference `{ id, revision }` |
|
|
69
|
+
| `requiredFeatures` | Effective task-profile features the application needs |
|
|
70
|
+
| `parameterValues` | Values that must be supported by the negotiated task |
|
|
71
|
+
| `architectures` | Architecture constraints |
|
|
72
|
+
| `parameters` | Optional `minBillions` / `maxBillions` bounds |
|
|
73
|
+
| `minimumContextLength` | Minimum context requirement |
|
|
74
|
+
| `memory.maximumRecommendedBytes` | Memory-related model selection constraint |
|
|
75
|
+
|
|
76
|
+
Task contract negotiation requires `features.taskProfiles`. Selection intents and file pins require `features.modelSelection`. The SDK rejects unsupported explicit requests rather than silently dropping their requirements.
|
|
77
|
+
|
|
78
|
+
#### Selection and substitution
|
|
79
|
+
|
|
80
|
+
`selection.version` is `1`. Its intent is `source-checkpoint`, `preferred-variant`, or `exact-artifact`. `preferred-variant` accompanies a `preferredModel`; `exact-artifact` also requires that model's immutable revision and complete selected `files` set.
|
|
81
|
+
|
|
82
|
+
| `substitution.automatic` | Meaning |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `none` | No automatic replacement of an explicit preferred model |
|
|
85
|
+
| `certified-only` | Automatic alternatives are limited to the site's `certifiedModels` |
|
|
86
|
+
| `same-checkpoint` | Allow eligible representations of the same source checkpoint |
|
|
87
|
+
| `compatible` | Allow a compatible replacement validated by Oryn |
|
|
88
|
+
|
|
89
|
+
These policies apply to automatic substitution; trusted user choice is separate. The request type constrains which policies apply: capability-only requests accept `compatible`, while source-checkpoint requests accept `none`, `same-checkpoint`, or `compatible`. Always inspect `session.resolution`.
|
|
90
|
+
|
|
91
|
+
With substitution omitted, an explicit model preference defaults to `none`, a source-checkpoint request defaults to `same-checkpoint`, and a capability-only request uses `compatible` selection.
|
|
92
|
+
|
|
93
|
+
#### Options and progress
|
|
94
|
+
|
|
95
|
+
| Option | Type | Behavior |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `signal` | `AbortSignal` | Requests cancellation of model resolution/preparation |
|
|
98
|
+
| `onProgress` | `(progress: ModelRequestProgress) => void` | Receives request progress while it is pending |
|
|
99
|
+
|
|
100
|
+
Stages are `resolving`, `awaiting-permission`, `awaiting-model-choice`, `downloading`, `loading`, and `ready`. Downloading includes `downloadedBytes`, optional `totalBytes`, and the model reference. Loading may include a numeric `progress`. Not every request visits every stage; an installed model may resolve quickly.
|
|
101
|
+
|
|
102
|
+
#### Returned session
|
|
103
|
+
|
|
104
|
+
| Field | Meaning |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `sessionId` | Opaque handle used in invocations |
|
|
107
|
+
| `role`, `origin` | Role and owning origin |
|
|
108
|
+
| `resolution` | Preferred/resolved models, compatibility, executability, trust, and substitution explanation |
|
|
109
|
+
| `capabilities` | Effective model capabilities |
|
|
110
|
+
| `taskProfile?` | Negotiated task contract, effective features, parameters, and limits |
|
|
111
|
+
|
|
112
|
+
`resolution.trust` is `oryn-verified` or `user-approved-unverified`. `developerCertified` is a separate declaration about the site's testing. Never present an unverified model as Oryn-certified.
|
|
113
|
+
|
|
114
|
+
`resolution.selection`, when present, explains whether selection chose the requested artifact, another representation of the same checkpoint, or a different model. Its source/confidence are advisory explanations, not runtime authorization.
|
|
115
|
+
|
|
116
|
+
### `client.models.releaseSession`
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
client.models.releaseSession(sessionId) → Promise<void>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Releases the site's model session. Do not reuse the ID afterward. Session release is separate from explicit job cancellation and artifact release; clean up each resource you own. A browser restart is not a supported session persistence mechanism.
|
|
123
|
+
|
|
124
|
+
## Jobs and events
|
|
125
|
+
|
|
126
|
+
### `client.invoke`
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
client.invoke<TInput>(request) → Promise<JobReference>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`request` contains `{ sessionId, input }`. `input` is the negotiated task's JSON payload, containing artifact IDs for media. The return value contains `jobId`, `sessionId`, initial `state`, and optional `queue` information. It is an acknowledgement of submission, not the result.
|
|
133
|
+
|
|
134
|
+
Shared-task inputs known to the SDK are validated before bridge invocation. Browser and Engine still enforce authorization, contract compatibility, artifact bindings, and runtime limits.
|
|
135
|
+
|
|
136
|
+
### `client.subscribe`
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
client.subscribe(listener) → () => void
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Registers a job-event listener and returns an unsubscribe function. Outside Oryn it returns a no-op unsubscribe. Subscribe **before** invoking and route by `jobId`; events can arrive before the invocation promise resolves. Helpers handle this early-event buffering for you.
|
|
143
|
+
|
|
144
|
+
All events have `event`, `timestamp`, `jobId`, `sessionId`, and `sequence`. Sequence values increase within a job. A job has one terminal outcome.
|
|
145
|
+
|
|
146
|
+
| Event | `data` | Terminal? |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| `job.state` | `state`, optional `queue` | No |
|
|
149
|
+
| `job.queue.updated` | `lane`, `position`, `jobsAhead`, `reason` | No |
|
|
150
|
+
| `job.progress` | Optional `progress` and `stage` | No |
|
|
151
|
+
| `job.output.delta` | `channel`, `delta` | No |
|
|
152
|
+
| `job.preview` | `artifactId` | No |
|
|
153
|
+
| `job.completed` | `result` | Yes |
|
|
154
|
+
| `job.failed` | Serialized `error` | Yes |
|
|
155
|
+
| `job.cancelled` | Optional `reason` | Yes |
|
|
156
|
+
|
|
157
|
+
Only consume text deltas when `channel === "text"`; the delta type is `unknown` because channels can carry different data. Render generated text using text nodes or your framework's escaped text rendering.
|
|
158
|
+
|
|
159
|
+
The current generation lane is global across origins, with one running generation job. Websites only receive their own job events. Use `scheduler` and queue events for UI instead of assuming immediate execution.
|
|
160
|
+
|
|
161
|
+
### `invokeAndWait`
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
invokeAndWait<TInput, TResult>(client, request, options?) → Promise<TResult>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Subscribes, submits, filters to the new job, and waits for a terminal event. `TResult` extends `InferenceResult` and defaults to it.
|
|
168
|
+
|
|
169
|
+
| Option | Behavior |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| `signal` | Requests job cancellation after the helper has the job reference |
|
|
172
|
+
| `onEvent` | Receives events for this job, including the terminal event |
|
|
173
|
+
|
|
174
|
+
- `job.completed`: resolves with its result.
|
|
175
|
+
- `job.failed`: rejects with `LocalAIError`.
|
|
176
|
+
- `job.cancelled`: rejects with `LocalAIError` code `REQUEST_ABORTED`.
|
|
177
|
+
|
|
178
|
+
Keep event callbacks lightweight and non-throwing. The generic result type is a TypeScript expectation, not a runtime validator for arbitrary returned JSON.
|
|
179
|
+
|
|
180
|
+
### `invokeStream`
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
invokeStream<TInput>(client, request, options?) → AsyncIterable<LocalAIEvent>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Accepts optional `{ signal }`, subscribes before submission, and yields events for this job through its terminal event. **Terminal `job.failed` and `job.cancelled` are yielded, not automatically thrown.** Submission errors can still reject iteration. A signal already aborted when iteration begins causes `REQUEST_ABORTED`.
|
|
187
|
+
|
|
188
|
+
The iterator starts when consumed. Closing it unsubscribes its listener; it does not cancel the running job. Use the signal while it is active or cancel a known job explicitly.
|
|
189
|
+
|
|
190
|
+
### `client.cancel`
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
client.cancel(jobId) → Promise<void>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Requests cancellation of an owned queued or running job. A successful call is not proof the runtime has already stopped; use the terminal event to settle the UI. Some runtimes interrupt only at safe execution points.
|
|
197
|
+
|
|
198
|
+
## Results
|
|
199
|
+
|
|
200
|
+
`InferenceResult` is a discriminated union:
|
|
201
|
+
|
|
202
|
+
| Kind | Payload | Typical use |
|
|
203
|
+
| --- | --- | --- |
|
|
204
|
+
| `structured` | `{ kind: "structured", value: unknown }` | Text, predictions, timestamps, embeddings |
|
|
205
|
+
| `artifact` | `{ kind: "artifact", artifact: ArtifactMetadata }` | Generated image/audio/video |
|
|
206
|
+
| `multi` | `{ kind: "multi", outputs: InferenceResult[] }` | Multiple images, layers, or media plus metadata |
|
|
207
|
+
|
|
208
|
+
Check `kind` before accessing fields. Check or validate task-specific structured values at your application boundary. `SharedTaskOutputs` mixes structured **value** types with complete artifact/multi envelopes, depending on the task; do not wrap every mapped type in another `structured` envelope.
|
|
209
|
+
|
|
210
|
+
## Artifacts
|
|
211
|
+
|
|
212
|
+
Artifacts are scoped capabilities for binary data. IDs/URLs are opaque and temporary. They do not grant global filesystem access.
|
|
213
|
+
|
|
214
|
+
| Method | Returns | Notes |
|
|
215
|
+
| --- | --- | --- |
|
|
216
|
+
| `artifacts.create(input, options?)` | `Promise<ArtifactReference>` | Accepts a `Blob` or `File`; supports `name` and `lifetime: "ephemeral" \| "session"` |
|
|
217
|
+
| `artifacts.get(id)` | `Promise<ArtifactMetadata>` | Reads authorized metadata |
|
|
218
|
+
| `artifacts.getUrl(id)` | `Promise<string>` | Gets a URL usable by media elements or fetching |
|
|
219
|
+
| `artifacts.readBlob(id)` | `Promise<Blob>` | Materializes the complete artifact in renderer memory |
|
|
220
|
+
| `artifacts.saveAs(id, options?)` | `Promise<void>` | Opens trusted save UI; optional `suggestedFilename` |
|
|
221
|
+
| `artifacts.release(id)` | `Promise<void>` | Releases the website's capability |
|
|
222
|
+
|
|
223
|
+
`ArtifactReference` contains `id`, `mimeType`, `sizeBytes`, and optional `name`. `ArtifactMetadata` adds `createdAt`, optional `expiresAt`, and optional `url`.
|
|
224
|
+
|
|
225
|
+
The current SDK copies inputs in 256 KiB chunks. Large media belongs in artifacts rather than base64 strings or large number arrays in invocation JSON. A file extension or a file picker's `accept` attribute is not validation of the actual encoding.
|
|
226
|
+
|
|
227
|
+
Keep an artifact alive while a media element is using its URL. `readBlob()` can be expensive for large videos; prefer direct media URLs for playback. `saveAs()` does not return the chosen local filesystem path. Uploading a blob with `fetch()` is a separate website decision.
|
|
228
|
+
|
|
229
|
+
## Task profiles and typed validation
|
|
230
|
+
|
|
231
|
+
`TaskProfile` includes:
|
|
232
|
+
|
|
233
|
+
- `contract: { id, revision }`, `task`, `profileId`, and `schemaVersion`;
|
|
234
|
+
- input/output modalities and `inputSchemaRef` / `resultSchemaRef` identifiers;
|
|
235
|
+
- feature states: `supported`, `unsupported`, `unknown`, or `requires-input`;
|
|
236
|
+
- parameters with `inputPointer`, `valueSchema`, optional `default`, and `handling`;
|
|
237
|
+
- `limits`, `streamingChannels`, and runtime provenance.
|
|
238
|
+
|
|
239
|
+
Parameter handling is `honored`, `fixed`, `unsupported`, or `requires-input`. Use this information to build controls that reflect the selected model. Schema references identify negotiated schemas; they are not automatically fetchable website URLs.
|
|
240
|
+
|
|
241
|
+
### `validateSharedTaskInput`
|
|
242
|
+
|
|
243
|
+
```text
|
|
244
|
+
validateSharedTaskInput(contract, input) → SharedTaskInputs[contract]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Checks a supported shared-task input against its generated schema, cross-field constraints, and JSON-size bound. Returns the typed input on success. Failure throws a validation error; do not assume every local failure is a `LocalAIError`. It does not request a model, grant permission, validate artifact ownership, or certify execution.
|
|
248
|
+
|
|
249
|
+
Exports include `SharedTaskInputs`, `SharedTaskOutputs`, `SharedTaskContract`, `SHARED_TASK_CONTRACTS`, and `SHARED_MODEL_TASK_CONTRACTS`. Additional public types include `TaskContractRef`, `TaskFeatureState`, `TaskParameter`, `TaskProfile`, `TaskRequirements`, `ModelSelection`, and `SelectionExplanation`.
|
|
250
|
+
|
|
251
|
+
## Errors
|
|
252
|
+
|
|
253
|
+
`LocalAIError` has `name: "LocalAIError"`, a stable `code`, `message`, and optional sanitized `details`. Construct it from a serialized error, for example when converting a `job.failed` event into a thrown exception.
|
|
254
|
+
|
|
255
|
+
| Category | Representative codes |
|
|
256
|
+
| --- | --- |
|
|
257
|
+
| Availability | `RUNTIME_NOT_AVAILABLE`, `RUNTIME_STARTING`, `RUNTIME_DEPENDENCY_UNAVAILABLE`, `RUNTIME_PROTOCOL_ERROR` |
|
|
258
|
+
| Permission/origin | `USER_DENIED`, `PERMISSION_DENIED`, `UNAUTHORIZED_ORIGIN`, `UNAUTHORIZED_FRAME` |
|
|
259
|
+
| Model selection | `MODEL_NOT_FOUND`, `MODEL_REFERENCE_INVALID`, `NO_EXECUTABLE_MODEL`, `MODEL_NOT_EXECUTABLE` |
|
|
260
|
+
| Model safety | `MODEL_SECURITY_BLOCKED`, `CUSTOM_CODE_REQUIRED`, `UNSAFE_MODEL_FORMAT` |
|
|
261
|
+
| Resources | `RESOURCE_REQUIREMENTS_UNSATISFIABLE`, `RESOURCE_LIMIT_EXCEEDED`, `DISK_SPACE_INSUFFICIENT`, `MODEL_OUT_OF_MEMORY` |
|
|
262
|
+
| Sessions | `SESSION_NOT_FOUND`, `SESSION_ACCESS_DENIED`, `SESSION_RELEASED` |
|
|
263
|
+
| Jobs | `JOB_QUEUE_FULL`, `JOB_INPUT_INVALID`, `JOB_EXECUTION_FAILED`, `JOB_CANCELLED`, `REQUEST_ABORTED` |
|
|
264
|
+
| Artifacts | `ARTIFACT_NOT_FOUND`, `ARTIFACT_ACCESS_DENIED`, `ARTIFACT_EXPIRED`, `ARTIFACT_INVALID` |
|
|
265
|
+
| Task-specific | `AUDIO_FORMAT_UNSUPPORTED`, `ASR_INFERENCE_FAILED`, `MODEL_OUTPUT_FILTERED`, `SONG_GENERATION_FAILED` |
|
|
266
|
+
|
|
267
|
+
Use the exported `LocalAIErrorCode` union for the complete compile-time list. Keep a fallback for unfamiliar future codes and ordinary JavaScript/network errors. Avoid logging user inputs or complete model outputs simply to diagnose a failure.
|
|
268
|
+
|
|
269
|
+
## Cancellation and cleanup
|
|
270
|
+
|
|
271
|
+
Model preparation, job execution, model sessions, and artifacts have separate lifetimes:
|
|
272
|
+
|
|
273
|
+
1. Abort a pending model request with its `signal`.
|
|
274
|
+
2. Cancel a job with its helper's active signal or `cancel(jobId)`.
|
|
275
|
+
3. Observe the terminal outcome before declaring the job stopped.
|
|
276
|
+
4. Release artifacts after playback, saving, or other consumers finish.
|
|
277
|
+
5. Release the session when the feature no longer needs the model.
|
|
278
|
+
6. Unsubscribe custom listeners when their owner is disposed.
|
|
279
|
+
|
|
280
|
+
Cleanup in `finally` prevents a successful or failed invocation from leaving an unused session behind. If cleanup itself fails, decide explicitly how to report that without replacing the original user-facing error. A stream `break`, a tab switch, and a session release should not be used as substitutes for explicit cancellation.
|
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1120" height="1150" viewBox="0 0 1120 1150" role="img" aria-labelledby="title description">
|
|
2
|
+
<title id="title">Oryn SDK request lifecycle</title><desc id="description">Detect capabilities; request a model and allow Browser consent and Engine preparation; receive a session; submit a queued job; consume events and a completed, failed, or cancelled terminal outcome; release sessions and artifacts. Failures or cancellation can also occur during preparation.</desc>
|
|
3
|
+
<defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#b2adff"/></marker></defs>
|
|
4
|
+
<rect width="1120" height="1150" rx="24" fill="#0c0e14"/>
|
|
5
|
+
<g font-family="Arial, Helvetica, sans-serif">
|
|
6
|
+
<text x="48" y="52" font-size="15" font-weight="700" fill="#b2adff" letter-spacing="2">ORYN SDK / REQUEST LIFECYCLE</text>
|
|
7
|
+
<text x="48" y="111" font-size="40" font-weight="700" fill="#f1f3fa" >From intent to a local result.</text>
|
|
8
|
+
<text x="48" y="151" font-size="21" font-weight="400" fill="#b4bed2" >The website asks. Oryn mediates. The user stays in control.</text>
|
|
9
|
+
<path d="M 80 289 V 333" fill="none" stroke="#b2adff" stroke-width="2" marker-end="url(#arrow)"/>
|
|
10
|
+
<circle cx="80" cy="237" r="29" fill="#24243e" stroke="#655e9b"/>
|
|
11
|
+
<text x="80" y="244" font-size="20" font-weight="700" fill="#b2adff" text-anchor="middle">01</text>
|
|
12
|
+
<rect x="134" y="201" width="938" height="115" rx="18" fill="#151923" stroke="#333b50" stroke-width="1.5"/>
|
|
13
|
+
<text x="158" y="235" font-size="24" font-weight="700" fill="#f1f3fa" >Detect</text>
|
|
14
|
+
<text x="1048" y="233" font-size="12" font-weight="700" fill="#b2adff" text-anchor="end" letter-spacing="1">WEBSITE</text>
|
|
15
|
+
<text x="158" y="266" font-size="18" font-weight="400" fill="#b2adff" font-family="Menlo, Consolas, monospace">getCapabilities()</text>
|
|
16
|
+
<text x="158" y="297" font-size="18" font-weight="400" fill="#b4bed2" >Check availability; offer a fallback.</text>
|
|
17
|
+
<path d="M 80 426 V 470" fill="none" stroke="#b2adff" stroke-width="2" marker-end="url(#arrow)"/>
|
|
18
|
+
<circle cx="80" cy="374" r="29" fill="#24243e" stroke="#655e9b"/>
|
|
19
|
+
<text x="80" y="381" font-size="20" font-weight="700" fill="#b2adff" text-anchor="middle">02</text>
|
|
20
|
+
<rect x="134" y="338" width="938" height="115" rx="18" fill="#151923" stroke="#333b50" stroke-width="1.5"/>
|
|
21
|
+
<text x="158" y="372" font-size="24" font-weight="700" fill="#f1f3fa" >Resolve</text>
|
|
22
|
+
<text x="1048" y="370" font-size="12" font-weight="700" fill="#b2adff" text-anchor="end" letter-spacing="1">BROWSER + ENGINE</text>
|
|
23
|
+
<text x="158" y="403" font-size="18" font-weight="400" fill="#b2adff" font-family="Menlo, Consolas, monospace">models.request(...)</text>
|
|
24
|
+
<text x="158" y="434" font-size="18" font-weight="400" fill="#b4bed2" >Permission, model choice, download and load.</text>
|
|
25
|
+
<path d="M 80 563 V 607" fill="none" stroke="#b2adff" stroke-width="2" marker-end="url(#arrow)"/>
|
|
26
|
+
<circle cx="80" cy="511" r="29" fill="#24243e" stroke="#655e9b"/>
|
|
27
|
+
<text x="80" y="518" font-size="20" font-weight="700" fill="#b2adff" text-anchor="middle">03</text>
|
|
28
|
+
<rect x="134" y="475" width="938" height="115" rx="18" fill="#151923" stroke="#333b50" stroke-width="1.5"/>
|
|
29
|
+
<text x="158" y="509" font-size="24" font-weight="700" fill="#f1f3fa" >Receive a session</text>
|
|
30
|
+
<text x="1048" y="507" font-size="12" font-weight="700" fill="#b2adff" text-anchor="end" letter-spacing="1">WEBSITE</text>
|
|
31
|
+
<text x="158" y="540" font-size="18" font-weight="400" fill="#b2adff" font-family="Menlo, Consolas, monospace">ModelSession</text>
|
|
32
|
+
<text x="158" y="571" font-size="18" font-weight="400" fill="#b4bed2" >Inspect the resolved model and task profile.</text>
|
|
33
|
+
<path d="M 80 700 V 744" fill="none" stroke="#b2adff" stroke-width="2" marker-end="url(#arrow)"/>
|
|
34
|
+
<circle cx="80" cy="648" r="29" fill="#24243e" stroke="#655e9b"/>
|
|
35
|
+
<text x="80" y="655" font-size="20" font-weight="700" fill="#b2adff" text-anchor="middle">04</text>
|
|
36
|
+
<rect x="134" y="612" width="938" height="115" rx="18" fill="#151923" stroke="#333b50" stroke-width="1.5"/>
|
|
37
|
+
<text x="158" y="646" font-size="24" font-weight="700" fill="#f1f3fa" >Submit a job</text>
|
|
38
|
+
<text x="1048" y="644" font-size="12" font-weight="700" fill="#b2adff" text-anchor="end" letter-spacing="1">SDK + ENGINE</text>
|
|
39
|
+
<text x="158" y="677" font-size="18" font-weight="400" fill="#b2adff" font-family="Menlo, Consolas, monospace">invokeAndWait() / invokeStream()</text>
|
|
40
|
+
<text x="158" y="708" font-size="18" font-weight="400" fill="#b4bed2" >Helpers subscribe before submitting; work may queue.</text>
|
|
41
|
+
<path d="M 80 837 V 881" fill="none" stroke="#b2adff" stroke-width="2" marker-end="url(#arrow)"/>
|
|
42
|
+
<circle cx="80" cy="785" r="29" fill="#24243e" stroke="#655e9b"/>
|
|
43
|
+
<text x="80" y="792" font-size="20" font-weight="700" fill="#b2adff" text-anchor="middle">05</text>
|
|
44
|
+
<rect x="134" y="749" width="938" height="115" rx="18" fill="#151923" stroke="#333b50" stroke-width="1.5"/>
|
|
45
|
+
<text x="158" y="783" font-size="24" font-weight="700" fill="#f1f3fa" >Observe the outcome</text>
|
|
46
|
+
<text x="1048" y="781" font-size="12" font-weight="700" fill="#98ddc5" text-anchor="end" letter-spacing="1">WEBSITE</text>
|
|
47
|
+
<text x="158" y="814" font-size="18" font-weight="400" fill="#98ddc5" font-family="Menlo, Consolas, monospace">LocalAIEvent / InferenceResult</text>
|
|
48
|
+
<text x="158" y="845" font-size="18" font-weight="400" fill="#b4bed2" >Progress, text deltas, previews, then one terminal event.</text>
|
|
49
|
+
<circle cx="80" cy="922" r="29" fill="#24243e" stroke="#655e9b"/>
|
|
50
|
+
<text x="80" y="929" font-size="20" font-weight="700" fill="#b2adff" text-anchor="middle">06</text>
|
|
51
|
+
<rect x="134" y="886" width="938" height="115" rx="18" fill="#151923" stroke="#333b50" stroke-width="1.5"/>
|
|
52
|
+
<text x="158" y="920" font-size="24" font-weight="700" fill="#f1f3fa" >Release resources</text>
|
|
53
|
+
<text x="1048" y="918" font-size="12" font-weight="700" fill="#b2adff" text-anchor="end" letter-spacing="1">WEBSITE</text>
|
|
54
|
+
<text x="158" y="951" font-size="18" font-weight="400" fill="#b2adff" font-family="Menlo, Consolas, monospace">releaseSession() / artifacts.release()</text>
|
|
55
|
+
<text x="158" y="982" font-size="18" font-weight="400" fill="#b4bed2" >Clean up when the feature and media consumers finish.</text>
|
|
56
|
+
<rect x="134" y="1034" width="938" height="77" rx="18" fill="#1a2230" stroke="#333b50" stroke-width="1.5"/>
|
|
57
|
+
<text x="158" y="1066" font-size="20" font-weight="700" fill="#98ddc5" >Terminal outcomes: completed · failed · cancelled</text>
|
|
58
|
+
<text x="158" y="1094" font-size="17" font-weight="400" fill="#b4bed2" >Requests can also fail or be cancelled before a session or job is created.</text>
|
|
59
|
+
</g></svg>
|
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# SDK recipes
|
|
2
|
+
|
|
3
|
+
[SDK overview](../README.md) · [API reference](api-reference.md)
|
|
4
|
+
|
|
5
|
+
These examples use the website API. They require a compatible Oryn Browser, an executable model, and the user's permission. Functions are intended to be called from your application's user actions; connect their progress and cancellation callbacks to your own UI.
|
|
6
|
+
|
|
7
|
+
## Choose a model
|
|
8
|
+
|
|
9
|
+
Prefer a task-only request when the exact model does not matter. If your application has a tested preference, declare it and an explicit substitution policy:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import type { LocalAIClient } from "@ocno/oryn-sdk";
|
|
13
|
+
|
|
14
|
+
export async function requestChat(localAI: LocalAIClient, signal: AbortSignal) {
|
|
15
|
+
const session = await localAI.models.request(
|
|
16
|
+
{
|
|
17
|
+
role: "chat",
|
|
18
|
+
preferredModel: {
|
|
19
|
+
provider: "huggingface",
|
|
20
|
+
repository: "Qwen/Qwen3.5-0.8B",
|
|
21
|
+
},
|
|
22
|
+
requirements: {
|
|
23
|
+
task: "text-generation",
|
|
24
|
+
inputModalities: ["text"],
|
|
25
|
+
outputModalities: ["text"],
|
|
26
|
+
capabilities: { chat: true, streaming: true },
|
|
27
|
+
},
|
|
28
|
+
substitution: { automatic: "compatible" },
|
|
29
|
+
},
|
|
30
|
+
{ signal, onProgress: (progress) => console.info(progress.stage) },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
console.info({
|
|
34
|
+
model: session.resolution.resolvedModel.repository,
|
|
35
|
+
revision: session.resolution.resolvedModel.revision,
|
|
36
|
+
substituted: session.resolution.substituted,
|
|
37
|
+
trust: session.resolution.trust,
|
|
38
|
+
});
|
|
39
|
+
return session; // The caller owns and must eventually release this session.
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The model name illustrates intent; it is not a guarantee that this checkpoint runs on every installation. Oryn may choose a compatible substitute or reject the request. Only put alternatives in `certifiedModels` when your application has actually tested them.
|
|
44
|
+
|
|
45
|
+
### Select a checkpoint or an exact artifact
|
|
46
|
+
|
|
47
|
+
When `features.modelSelection` is available, `source-checkpoint` allows selection of an eligible representation of the same checkpoint. `preferred-variant` accompanies a preferred model. `exact-artifact` additionally pins an immutable revision and a complete selected file list.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import type { LocalAIClient, ModelRef } from "@ocno/oryn-sdk";
|
|
51
|
+
|
|
52
|
+
export async function requestExactChat(
|
|
53
|
+
localAI: LocalAIClient,
|
|
54
|
+
model: ModelRef,
|
|
55
|
+
signal: AbortSignal,
|
|
56
|
+
) {
|
|
57
|
+
if (!(await localAI.getCapabilities()).features.modelSelection) {
|
|
58
|
+
throw new Error("This browser does not support exact model selection.");
|
|
59
|
+
}
|
|
60
|
+
if (!model.files?.length || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(model.revision ?? "")) {
|
|
61
|
+
throw new Error("Supply an immutable model revision and its selected files.");
|
|
62
|
+
}
|
|
63
|
+
return localAI.models.request(
|
|
64
|
+
{
|
|
65
|
+
role: "chat",
|
|
66
|
+
preferredModel: model,
|
|
67
|
+
selection: { version: 1, intent: "exact-artifact" },
|
|
68
|
+
requirements: { task: "text-generation", capabilities: { chat: true } },
|
|
69
|
+
substitution: { automatic: "none" },
|
|
70
|
+
},
|
|
71
|
+
{ signal },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The caller supplies a real model identity and owns the returned session. Inspect the returned resolution even for an exact preference: automatic substitution policy and the person's trusted model choice are separate concerns.
|
|
77
|
+
|
|
78
|
+
## Require an effective task feature
|
|
79
|
+
|
|
80
|
+
Use a versioned contract when the feature is essential. Fail visibly on older browsers instead of silently downgrading requirements:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import type { LocalAIClient } from "@ocno/oryn-sdk";
|
|
84
|
+
|
|
85
|
+
export async function requestStreamingChat(localAI: LocalAIClient) {
|
|
86
|
+
if (!(await localAI.getCapabilities()).features.taskProfiles) {
|
|
87
|
+
throw new Error("This feature requires task-profile support in Oryn.");
|
|
88
|
+
}
|
|
89
|
+
const session = await localAI.models.request({
|
|
90
|
+
role: "chat",
|
|
91
|
+
requirements: {
|
|
92
|
+
task: "text-generation",
|
|
93
|
+
outputModalities: ["text"],
|
|
94
|
+
contract: { id: "chat-v1", revision: 1 },
|
|
95
|
+
requiredFeatures: ["streaming"],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
console.info(session.taskProfile?.streamingChannels);
|
|
99
|
+
console.info(session.taskProfile?.parameters);
|
|
100
|
+
return session; // Release when the conversation UI is disposed.
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A profile can mark a parameter as fixed or unsupported. Render only controls the profile can honor. Feature states of `unknown` or `requires-input` are not unconditional support.
|
|
105
|
+
|
|
106
|
+
## Transcribe a WAV file
|
|
107
|
+
|
|
108
|
+
This recipe expects a PCM WAV file. The header check catches obvious container mismatches; Engine validates the actual audio format. It requests transcription, creates an input artifact, reads a structured text result, and cleans up both resources.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import {
|
|
112
|
+
createLocalAIClient,
|
|
113
|
+
invokeAndWait,
|
|
114
|
+
} from "@ocno/oryn-sdk";
|
|
115
|
+
|
|
116
|
+
export async function transcribeWav(file: File, signal: AbortSignal): Promise<string> {
|
|
117
|
+
const header = new Uint8Array(await file.slice(0, 12).arrayBuffer());
|
|
118
|
+
const ascii = new TextDecoder("ascii");
|
|
119
|
+
if (
|
|
120
|
+
header.length < 12 ||
|
|
121
|
+
ascii.decode(header.subarray(0, 4)) !== "RIFF" ||
|
|
122
|
+
ascii.decode(header.subarray(8, 12)) !== "WAVE"
|
|
123
|
+
) {
|
|
124
|
+
throw new Error("Choose a PCM WAV recording.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const localAI = createLocalAIClient();
|
|
128
|
+
const capabilities = await localAI.getCapabilities();
|
|
129
|
+
if (!capabilities.runtimeAvailable) throw new Error("Local AI is unavailable.");
|
|
130
|
+
|
|
131
|
+
const session = await localAI.models.request(
|
|
132
|
+
{
|
|
133
|
+
role: "transcription",
|
|
134
|
+
preferredModel: { provider: "huggingface", repository: "openai/whisper-small" },
|
|
135
|
+
requirements: {
|
|
136
|
+
task: "automatic-speech-recognition",
|
|
137
|
+
inputModalities: ["audio"],
|
|
138
|
+
outputModalities: ["text"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{ signal },
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
let inputId: string | undefined;
|
|
145
|
+
try {
|
|
146
|
+
const input = await localAI.artifacts.create(file, { lifetime: "session" });
|
|
147
|
+
inputId = input.id;
|
|
148
|
+
const result = await invokeAndWait(localAI, {
|
|
149
|
+
sessionId: session.sessionId,
|
|
150
|
+
input: { audioArtifactId: input.id, language: "auto", timestamps: false },
|
|
151
|
+
}, { signal });
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
result.kind !== "structured" ||
|
|
155
|
+
typeof result.value !== "object" || result.value === null ||
|
|
156
|
+
!("text" in result.value) || typeof result.value.text !== "string"
|
|
157
|
+
) {
|
|
158
|
+
throw new Error("Unexpected transcription result.");
|
|
159
|
+
}
|
|
160
|
+
return result.value.text;
|
|
161
|
+
} finally {
|
|
162
|
+
try {
|
|
163
|
+
if (inputId) await localAI.artifacts.release(inputId);
|
|
164
|
+
} finally {
|
|
165
|
+
await localAI.models.releaseSession(session.sessionId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Timestamps are disabled here. If your product requires word timestamps, negotiate `transcription-v1` with `requiredFeatures: ["wordTimestamps"]` and compatible parameters, then inspect the returned profile. A text-only task match does not promise word timing.
|
|
172
|
+
|
|
173
|
+
## Use a typed task contract
|
|
174
|
+
|
|
175
|
+
The SDK exports typed input/output mappings for additional tasks. For zero-shot classification, the mapped output is the **value inside** a structured result:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import {
|
|
179
|
+
invokeAndWait,
|
|
180
|
+
validateSharedTaskInput,
|
|
181
|
+
type LocalAIClient,
|
|
182
|
+
type SharedTaskInputs,
|
|
183
|
+
type SharedTaskOutputs,
|
|
184
|
+
} from "@ocno/oryn-sdk";
|
|
185
|
+
|
|
186
|
+
type ClassificationResult = {
|
|
187
|
+
kind: "structured";
|
|
188
|
+
value: SharedTaskOutputs["zero-shot-classification-v1"];
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export async function classify(localAI: LocalAIClient, text: string) {
|
|
192
|
+
if (!(await localAI.getCapabilities()).features.taskProfiles) {
|
|
193
|
+
throw new Error("This feature requires task-profile support in Oryn.");
|
|
194
|
+
}
|
|
195
|
+
const input: SharedTaskInputs["zero-shot-classification-v1"] = {
|
|
196
|
+
text,
|
|
197
|
+
labels: ["support", "sales", "feedback"],
|
|
198
|
+
multiLabel: false,
|
|
199
|
+
};
|
|
200
|
+
validateSharedTaskInput("zero-shot-classification-v1", input);
|
|
201
|
+
|
|
202
|
+
const session = await localAI.models.request({
|
|
203
|
+
role: "classification",
|
|
204
|
+
requirements: {
|
|
205
|
+
task: "zero-shot-classification",
|
|
206
|
+
outputModalities: ["structured"],
|
|
207
|
+
contract: { id: "zero-shot-classification-v1", revision: 1 },
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
const result = await invokeAndWait<typeof input, ClassificationResult>(localAI, {
|
|
212
|
+
sessionId: session.sessionId,
|
|
213
|
+
input,
|
|
214
|
+
});
|
|
215
|
+
return result.value.predictions;
|
|
216
|
+
} finally {
|
|
217
|
+
await localAI.models.releaseSession(session.sessionId);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The generic result annotation is a compile-time expectation. Add runtime checks if your application accepts other result contracts. Other mapped outputs already contain an artifact or multi-result envelope; check the type for the task you are using.
|
|
223
|
+
|
|
224
|
+
## Display, save, and upload an output
|
|
225
|
+
|
|
226
|
+
A generated image or audio file can be displayed directly from its artifact URL. The following helper accepts an existing `ArtifactResult` from your chosen generation task and returns a cleanup function. Keep the artifact alive until the media element is finished with it.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import type { ArtifactResult, LocalAIClient } from "@ocno/oryn-sdk";
|
|
230
|
+
|
|
231
|
+
export async function displayOutput(
|
|
232
|
+
localAI: LocalAIClient,
|
|
233
|
+
result: ArtifactResult,
|
|
234
|
+
element: HTMLImageElement | HTMLAudioElement | HTMLVideoElement,
|
|
235
|
+
): Promise<() => Promise<void>> {
|
|
236
|
+
element.src = await localAI.artifacts.getUrl(result.artifact.id);
|
|
237
|
+
return async () => {
|
|
238
|
+
if (element instanceof HTMLMediaElement) element.pause();
|
|
239
|
+
element.removeAttribute("src");
|
|
240
|
+
if (element instanceof HTMLMediaElement) element.load();
|
|
241
|
+
await localAI.artifacts.release(result.artifact.id);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function saveOutput(localAI: LocalAIClient, result: ArtifactResult) {
|
|
246
|
+
await localAI.artifacts.saveAs(result.artifact.id, {
|
|
247
|
+
suggestedFilename: result.artifact.name ?? "oryn-output",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Call `saveOutput` from a Save button. Use a MIME-appropriate filename if the result has no name. For `multi` results, walk the `outputs` array and handle each envelope by its `kind`.
|
|
253
|
+
|
|
254
|
+
If your application also offers upload, make that a separate user action and clearly identify the destination. This function intentionally reads and uploads the complete artifact:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import type { ArtifactResult, LocalAIClient } from "@ocno/oryn-sdk";
|
|
258
|
+
|
|
259
|
+
export async function uploadOutput(localAI: LocalAIClient, result: ArtifactResult) {
|
|
260
|
+
const blob = await localAI.artifacts.readBlob(result.artifact.id);
|
|
261
|
+
const body = new FormData();
|
|
262
|
+
body.append("file", blob, result.artifact.name ?? "oryn-output");
|
|
263
|
+
const response = await fetch("/api/uploads", { method: "POST", body });
|
|
264
|
+
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
|
|
265
|
+
return response;
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`/api/uploads` is an endpoint your website must implement; it is not an Oryn service. The caller still owns the artifact and should release it when all display/save/upload consumers have finished. `readBlob()` holds the whole output in renderer memory, which matters for large media.
|
|
270
|
+
|
|
271
|
+
## Build a cancellation UI
|
|
272
|
+
|
|
273
|
+
For the streaming quickstart, create an `AbortController` per submission and pass its signal to `streamReply`. Wire the Cancel button to `controller.abort()`. For multiple jobs, keep separate controllers so one click does not cancel unrelated work.
|
|
274
|
+
|
|
275
|
+
Keep queue and model-preparation states visible. Cancelling a request and cancelling an already submitted job are separate operations. A cancellation request can take time to stop the runtime; wait for the terminal outcome before reporting that it has finished. If you manage submission yourself, retain the `jobId` and use `localAI.cancel(jobId)`.
|
|
276
|
+
|
|
277
|
+
Unsubscribing or breaking out of an async iterator only stops your event consumer. It does not stop inference. A page should clean up event listeners, then release sessions and artifacts according to their actual lifetime.
|
|
278
|
+
|
|
279
|
+
## Integrate with a framework
|
|
280
|
+
|
|
281
|
+
The package has no framework dependency. Create a client in a module or component, run feature work only on the client, and let the framework own rendering.
|
|
282
|
+
|
|
283
|
+
- **React / Vue / Svelte:** keep active controllers and sessions in component-owned state; use lifecycle cleanup for subscriptions and resource release.
|
|
284
|
+
- **SSR:** import and client creation are safe, but server execution reports no browser runtime. Detect again after hydration instead of caching a server-side availability result.
|
|
285
|
+
- **Rich text:** treat model output as untrusted text. Use escaped text rendering or your application's established sanitization before rendering markup.
|
|
286
|
+
- **Fallback:** explain when Local AI is unavailable. If you offer a cloud alternative, make its data destination and user choice clear.
|
|
287
|
+
|
|
288
|
+
The reference applications in the source distribution show full page UI and progress handling. These recipes focus on the SDK calls that those applications share.
|