apple-llm 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 apple-llm contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # apple-llm
2
+
3
+ Apple's on-device and Private Cloud Compute LLMs, from Node. No API key, no
4
+ account, no developer program membership.
5
+
6
+ ```bash
7
+ npm install apple-llm
8
+ ```
9
+
10
+ ```ts
11
+ import { AppleLLM, probe } from 'apple-llm';
12
+
13
+ await probe();
14
+ // { device: { available, contextSize, variant }, cloud: { available, installed } }
15
+
16
+ const llm = new AppleLLM({ tier: 'device' }); // 'device' | 'cloud' | 'auto'
17
+ await llm.text('Summarize this', { system: 'You are terse.' });
18
+ await llm.json('Extract the fields', { schema }); // guaranteed to match, on device
19
+ llm.close();
20
+ ```
21
+
22
+ ESM + CJS, fully typed, **zero runtime dependencies**. A small Swift helper is
23
+ compiled on first use and cached; there is no postinstall script, so installing
24
+ on Linux or an Intel Mac always succeeds.
25
+
26
+ ## Read this before you use it
27
+
28
+ - **macOS 26+ on Apple Silicon.** No fallback anywhere else. Import always
29
+ works; `probe()` returns `available: false` with an actionable reason.
30
+ - **The on-device model is small** (~20B sparse, 1–4B active, 8192-token context
31
+ on macOS 27). Good at classification, extraction, tagging, rewriting and short
32
+ prose. **Bad at code generation and long reasoning.**
33
+ - **The cloud tier is not local.** It sends your prompt to Apple's Private Cloud
34
+ Compute, off your machine. Free but quota'd, reached through a private
35
+ Shortcuts action Apple can change in any OS release, and with no constrained
36
+ decoding — so `json()` there is a request, not a guarantee.
37
+ - **Streaming is text-only, tools are Apple's built-ins.** `stream()` gives
38
+ partials-as-they-arrive; `tools: ['ocr', 'barcode', 'spotlight']` enables
39
+ on-device Vision/Spotlight tools. Generic user-supplied function calling is
40
+ still a later version.
41
+
42
+ ## Two traps worth knowing
43
+
44
+ **Never set `temperature: 0`.** Constrained decoding already guarantees the
45
+ schema, so greedy decoding buys nothing and reliably degenerates — it padded an
46
+ unbounded array forever, then ran away inside a single string (2.7KB of
47
+ `"tasks-tasks-tasks-…"`), turning a 2s call into 20s. The default is `0.4`.
48
+ Apple honours `maxItems` but ignores `maxLength`: bound your arrays.
49
+
50
+ **Keep the client alive.** One long-lived helper process holds the model
51
+ resident; spawning one per call measured ~17s against ~1.5s. Construct
52
+ `AppleLLM` once, `close()` when you are done. Both traps produce *correct*
53
+ output, only slower, so neither looks like a bug.
54
+
55
+ Apple serialises inference regardless — 4 concurrent requests measured 29.19s
56
+ against 29.45s sequentially — so the request queue serialises deliberately.
57
+
58
+ ## What macOS 27 adds
59
+
60
+ ```ts
61
+ const { device, cloud } = await probe();
62
+ device.capabilities; // { vision, guidedGeneration, reasoning, toolCalling }
63
+ device.useCases; // ['general', 'contentTagging']
64
+ cloud.quota; // { status: 'belowLimit' | 'limitReached', approachingLimit, resetDate }
65
+ ```
66
+
67
+ On this machine the on-device model reports vision, guided generation and tool
68
+ calling, but **not** reasoning. `cloud.quota` is real quota state read from
69
+ `PrivateCloudComputeLanguageModel.quotaUsage` — the entitlement that blocks PCC
70
+ *inference* does not block reading it — so an exhausted quota becomes an
71
+ immediate `QuotaError` rather than a wasted Shortcuts round trip.
72
+
73
+ **Count tokens before sending**, turning a `ContextLengthError` into arithmetic:
74
+
75
+ ```ts
76
+ const { tokens, contextSize } = await llm.countTokens(prompt, { system });
77
+ ```
78
+
79
+ **Reproducible output without the `temperature: 0` trap.** Greedy decoding is
80
+ deterministic *and* degenerates; seeded top-k is deterministic and does not:
81
+
82
+ ```ts
83
+ await llm.text(prompt, { sampling: { mode: 'topK', k: 50, seed: 42 }, temperature: 0.9 });
84
+ ```
85
+
86
+ It works only because the helper uses a fresh session per request — reusing one
87
+ changes the transcript and with it the output.
88
+
89
+ **Vision**, by path — optionally labelled for follow-up turns. A missing file
90
+ is an error, never a silent drop:
91
+
92
+ ```ts
93
+ await llm.text('What is in this image?', { images: ['./photo.png'] });
94
+ await llm.text('What is in the image labelled chart?', {
95
+ images: [{ path: './scan.png', label: 'chart' }],
96
+ });
97
+ ```
98
+
99
+ **Streaming, conversations, tools, documents, and Write-with-Siri presets:**
100
+
101
+ ```ts
102
+ await llm.stream('Count to three.', { onDelta: (d) => process.stdout.write(d) });
103
+
104
+ const chat = llm.conversation('trip-planning');
105
+ await chat.text('My cat is called Biscuit.');
106
+ await chat.text('What is my cat called?'); // Biscuit.
107
+ await chat.history(); // mirrored turns, oldest first
108
+
109
+ await llm.text('Read the total.', { images: ['./receipt.png'], tools: ['ocr'] });
110
+ await llm.text('Which notes mention the bridge?', { tools: ['spotlight'] });
111
+ await llm.text('Compare these quotes.', { documents: ['./a.md', './b.md'] });
112
+
113
+ await llm.rewrite('gonna grab a bite', { instruction: 'Make it formal.' });
114
+ await llm.proofread('Their going to the store...');
115
+ await llm.summarize(longText);
116
+ await llm.askScreen('What is on this schedule?');
117
+ ```
118
+
119
+ **A tagging-specialised model**, `permissive` guardrails for rewriting, and
120
+ `prewarm()` to load model assets up front (worth little once they are resident —
121
+ 0.31s against 0.36s here — but real on a cold system):
122
+
123
+ ```ts
124
+ const llm = new AppleLLM({ tier: 'device', useCase: 'contentTagging' });
125
+ await new AppleLLM({ guardrails: 'permissive' }).text('Rewrite more formally: …');
126
+ await llm.prewarm();
127
+ ```
128
+
129
+ ## Errors
130
+
131
+ ```ts
132
+ import { QuotaError, ModelUnavailableError } from 'apple-llm';
133
+ ```
134
+
135
+ `ModelUnavailableError` (`.reason` is `appleIntelligenceNotEnabled` /
136
+ `modelNotReady` / `deviceNotEligible` / …), `SchemaRejectedError`,
137
+ `ContextLengthError`, `QuotaError` (`.resetDate`), `TimeoutError`,
138
+ `SetupRequiredError`, `RefusalError`.
139
+
140
+ ## CLI
141
+
142
+ ```bash
143
+ apple-llm probe
144
+ apple-llm setup-cloud [--web-search] # one-time, generates + signs locally
145
+ apple-llm run --tier device --system "You are terse." -
146
+ apple-llm run --tier cloud --schema schema.json "Extract the fields"
147
+ apple-llm run --image photo.png "What is in this image?"
148
+ apple-llm run --image scan.png::chart "What is in the image labelled chart?"
149
+ apple-llm run --tool ocr --image receipt.png "Read the total."
150
+ apple-llm run --document a.md --document b.md "Compare these."
151
+ apple-llm run --session trip --stream "What is my cat called?"
152
+ apple-llm history --session trip
153
+ apple-llm reset --session trip
154
+ apple-llm ask-screen "What is on this schedule?"
155
+ apple-llm rewrite "gonna grab a bite"
156
+ apple-llm run --use-case contentTagging --schema tags.json "A recipe for sourdough…"
157
+ apple-llm run --seed 42 "Reproducible output"
158
+ apple-llm count --system "You are terse." - # tokens before sending
159
+ ```
160
+
161
+ ## Schemas
162
+
163
+ `json()` runs your JSON Schema through `toAppleSchema()`, which rewrites it into
164
+ the restricted dialect Apple's `GenerationSchema` decoder accepts — eight rules
165
+ covering unions, `x-order`, enums, titles, `$ref`-by-title, `additionalProperties`,
166
+ string-typed `const`, and empty objects. See the
167
+ [full notes in the repo](https://github.com/jagdish/apple-llm#the-generationschema-dialect).
168
+
169
+ The details matter more than they look: constrained decoding makes a schema
170
+ mistake invisible but total. A union collapsed to the wrong branch does not warn
171
+ — it makes the right answer unreachable.
172
+
173
+ ## Credit
174
+
175
+ Extracted from [api-scribe](https://github.com/jagdish/api-scribe) (MIT), where
176
+ both routes were discovered and shipped. MIT.