apple-llm 0.1.0 → 0.2.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/CHANGELOG.md +94 -0
- package/README.md +251 -112
- package/dist/ai-sdk.cjs +281 -0
- package/dist/ai-sdk.d.cts +54 -0
- package/dist/ai-sdk.d.ts +54 -0
- package/dist/ai-sdk.js +281 -0
- package/dist/chunk-GM325EMJ.js +2584 -0
- package/dist/chunk-NRDZIP5G.cjs +2588 -0
- package/dist/chunk-OQATUZWF.cjs +81 -0
- package/dist/chunk-ZB4RDEPW.js +81 -0
- package/dist/cli.js +3163 -24
- package/dist/client-CXewzZTj.d.cts +1160 -0
- package/dist/client-CXewzZTj.d.ts +1160 -0
- package/dist/index.cjs +110 -1667
- package/dist/index.d.cts +84 -627
- package/dist/index.d.ts +84 -627
- package/dist/index.js +31 -1
- package/dist/server.cjs +361 -0
- package/dist/server.d.cts +51 -0
- package/dist/server.d.ts +51 -0
- package/dist/server.js +361 -0
- package/package.json +66 -14
- package/swift/helper.swift +748 -255
- package/dist/chunk-FQTRQ3KP.js +0 -1590
- package/dist/cli.cjs +0 -2027
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
The helper speaks a new protocol, so the first call after upgrading compiles it
|
|
6
|
+
once more (a few seconds). Every 0.1 call still works; the changes in behaviour
|
|
7
|
+
below are the ones worth reading before you upgrade.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Function tools.** `tool({ name, description, parameters, execute })` — the
|
|
12
|
+
model calls your JavaScript mid-generation, with arguments guaranteed to match
|
|
13
|
+
the parameter schema. Tools without `execute` stop generation with
|
|
14
|
+
`finishReason: 'tool-calls'` so you can run them yourself and continue with a
|
|
15
|
+
`tool` message. Mixes with the built-in `'ocr'`, `'barcode'`, `'spotlight'`.
|
|
16
|
+
`maxToolCalls` (default 8) stops a model that loops on a tool.
|
|
17
|
+
- **Standard Schema support.** Pass a Zod 4, ArkType or Valibot
|
|
18
|
+
(`toStandardJsonSchema`) schema anywhere a JSON Schema goes. `json()` is typed
|
|
19
|
+
by it and validated with it; a reply that fails validation is retried once
|
|
20
|
+
with the problems pointed out, then raised as `SchemaValidationError`.
|
|
21
|
+
- **Message lists.** Every method takes `ChatMessage[]` as well as a string.
|
|
22
|
+
History is sent as Apple's native transcript, and trimmed from the oldest end
|
|
23
|
+
if it outgrows the window (`trimHistory`, `result.trimmedTurns`).
|
|
24
|
+
- **`generate()`** returns everything about a call: `text`, `object`,
|
|
25
|
+
`finishReason` (`'stop' | 'length' | 'tool-calls'`), `usage` (input, output,
|
|
26
|
+
cached tokens — macOS 27), `toolCalls` with their outputs, `durationMs`, and
|
|
27
|
+
the assistant `message` to append for the next turn.
|
|
28
|
+
- **`for await` streaming.** `stream()` returns a stream you can iterate or
|
|
29
|
+
await; breaking out cancels generation. `streamJson()` streams partial objects
|
|
30
|
+
as the model fills them in.
|
|
31
|
+
- **Cancellation.** `signal` on every call stops the model itself, not just the
|
|
32
|
+
promise. `timeoutMs` per call or per client.
|
|
33
|
+
- **OpenAI-compatible server**: `apple-llm serve`, or `serve()` from
|
|
34
|
+
`apple-llm/server`. Chat completions with streaming, tools, `response_format`
|
|
35
|
+
JSON Schema and images. Localhost-only, no CORS and on-device by default.
|
|
36
|
+
- **Vercel AI SDK provider**: `apple()` from `apple-llm/ai-sdk`, for AI SDK 6
|
|
37
|
+
and 7 — `generateText`, `streamText`, `generateObject`, multi-step tools,
|
|
38
|
+
images.
|
|
39
|
+
- **`apple-llm chat`**, an interactive terminal chat. `run --json` prints the
|
|
40
|
+
full result; `run --schema --stream` prints partial objects, one per line;
|
|
41
|
+
`run --timeout`.
|
|
42
|
+
- **Long text.** `summarize()` handles text longer than the context window by
|
|
43
|
+
summarising it in parts.
|
|
44
|
+
- **Named sessions recover from overflow.** A conversation that outgrows the
|
|
45
|
+
window is rebuilt from its newest turns instead of failing forever, and after
|
|
46
|
+
a helper restart it resumes as a native transcript rather than a text
|
|
47
|
+
preamble.
|
|
48
|
+
- `countTokens()` now counts message history, schema and tools too.
|
|
49
|
+
- `using` / `await using` dispose a client. Error classes `AbortError`,
|
|
50
|
+
`ToolExecutionError` (your error is its `cause`), `SchemaValidationError`,
|
|
51
|
+
`ModelBusyError` and `UnsupportedError`, and a stable `code` on every error.
|
|
52
|
+
- Images on `user` messages, and images as bytes, data URLs or http URLs
|
|
53
|
+
through the server and the AI SDK provider.
|
|
54
|
+
- **The cloud tier on macOS 27 Golden Gate** reaches the next-generation server
|
|
55
|
+
model Siri AI is built on — the Shortcuts route already selected it, and this
|
|
56
|
+
release makes it visible and uses more of it: `probe().cloud.capabilities` and
|
|
57
|
+
`contextSize` come from the framework (reasoning, vision, tool calling, 32k),
|
|
58
|
+
and **the cloud tier now reads images**, passed as extra Shortcuts inputs with
|
|
59
|
+
the existing shortcut — no reinstall. Images are refused, not dropped, where
|
|
60
|
+
the server model's vision cannot be confirmed (macOS 26).
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- **A script no longer hangs without `close()`.** The helper is unref'd between
|
|
65
|
+
calls, so Node exits when your work is done. `close()` still releases it early.
|
|
66
|
+
- **`json()` fills in `null` for required nullable fields** the model left out.
|
|
67
|
+
Apple's dialect cannot express `null`, so such fields become optional on the
|
|
68
|
+
way in; your schema said they are present, so they are put back on the way
|
|
69
|
+
out. Code that checked `'key' in result` for these fields will now see them.
|
|
70
|
+
- **Schemas lose `pattern`** before reaching Apple's decoder, which rejects
|
|
71
|
+
every regex (`UnsupportedGuide`) — this is what made `z.string().email()`
|
|
72
|
+
fail. The pattern is checked on the reply instead.
|
|
73
|
+
- **The cloud tier streams** as a single chunk instead of throwing, accepts
|
|
74
|
+
`documents` and message lists, and now refuses `sessionId` and `tools` on
|
|
75
|
+
`json()` as it already did on `text()`, instead of ignoring them.
|
|
76
|
+
- `stream()` returns a `ResultStream` rather than a `Promise<string>`. Awaiting
|
|
77
|
+
it still gives the text.
|
|
78
|
+
- Helper errors of kind `unsupported` are `UnsupportedError`, a subclass of
|
|
79
|
+
`AppleLLMError`, so existing `instanceof AppleLLMError` checks still match.
|
|
80
|
+
- Concurrent first calls share one probe-and-compile instead of racing.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- Types resolve for CommonJS consumers (`require`) under `node16`/`nodenext`
|
|
85
|
+
module resolution, and the package passes publint and are-the-types-wrong.
|
|
86
|
+
- A single-entry `allOf` holding an inline object (rather than a `$ref`) was
|
|
87
|
+
sent to Apple as an object with no properties; it now keeps them. The same
|
|
88
|
+
fix covers nullable object unions, which are new in this release.
|
|
89
|
+
- A `Conversation`'s default `system` is no longer wiped by a call that passes
|
|
90
|
+
`system: undefined`.
|
|
91
|
+
- The helper's stderr is drained, so a chatty helper can no longer block on a
|
|
92
|
+
full pipe, and its last lines are quoted when it exits unexpectedly.
|
|
93
|
+
- A request turned away because other processes are using the model
|
|
94
|
+
(`ModelManagerError 1042`) is retried with backoff instead of failing.
|
package/README.md
CHANGED
|
@@ -1,176 +1,315 @@
|
|
|
1
1
|
# apple-llm
|
|
2
2
|
|
|
3
|
-
Apple's
|
|
4
|
-
account, no
|
|
3
|
+
Apple's built-in LLMs from Node — the on-device model and Private Cloud
|
|
4
|
+
Compute. No API key, no account, no model download, **zero runtime
|
|
5
|
+
dependencies**.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
npm install apple-llm
|
|
8
9
|
```
|
|
9
10
|
|
|
10
11
|
```ts
|
|
11
|
-
import { AppleLLM
|
|
12
|
+
import { AppleLLM } from 'apple-llm';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
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();
|
|
14
|
+
const llm = new AppleLLM();
|
|
15
|
+
await llm.text('Summarize this in one line: …');
|
|
20
16
|
```
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
on Linux or an Intel Mac always succeeds.
|
|
18
|
+
That is the whole setup. A small Swift helper compiles on first use (a few
|
|
19
|
+
seconds, once) and stays warm between calls.
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
- **Everything an LLM SDK should have** — streaming (`for await`), structured
|
|
22
|
+
output typed by Zod, tool calling with your own JavaScript functions,
|
|
23
|
+
multi-turn chat, cancellation, timeouts, token counting, usage.
|
|
24
|
+
- **Three ways in** — this library, an [OpenAI-compatible server](#openai-compatible-server)
|
|
25
|
+
for any tool that speaks OpenAI, and a [Vercel AI SDK provider](#vercel-ai-sdk).
|
|
26
|
+
- **Two tiers** — on-device (private, fast, free) and Apple's big server
|
|
27
|
+
model on Private Cloud Compute — on macOS 27 Golden Gate, the one Siri AI is
|
|
28
|
+
built on: reasoning, images, live web knowledge, 32k context. Free, no key,
|
|
29
|
+
behind the same API.
|
|
27
30
|
|
|
28
|
-
|
|
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.
|
|
31
|
+
## Read this first
|
|
41
32
|
|
|
42
|
-
|
|
33
|
+
- **macOS 26+ on Apple Silicon, with Apple Intelligence on.** Nothing works
|
|
34
|
+
elsewhere, but importing always succeeds and `probe()` says exactly why.
|
|
35
|
+
- **The on-device model is small** (~20B sparse, 1–4B active; 8,192-token
|
|
36
|
+
context). Good at extraction, classification, tagging, rewriting, summaries
|
|
37
|
+
and short answers. Bad at code and long reasoning. Use it for the jobs it
|
|
38
|
+
suits.
|
|
39
|
+
- **The cloud tier is not local.** `tier: 'cloud'` sends your prompt to Apple's
|
|
40
|
+
Private Cloud Compute. The default, `auto`, uses the device when it can.
|
|
43
41
|
|
|
44
|
-
|
|
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.
|
|
42
|
+
## Quick tour
|
|
49
43
|
|
|
50
|
-
|
|
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.
|
|
44
|
+
### Structured output, typed by your schema
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
## What macOS 27 adds
|
|
46
|
+
On device, the shape is **guaranteed** by constrained decoding — the model
|
|
47
|
+
cannot produce anything else. Pass a Zod 4 (or ArkType, or Valibot) schema and
|
|
48
|
+
the result is typed and validated; plain JSON Schema works too.
|
|
59
49
|
|
|
60
50
|
```ts
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
import { z } from 'zod';
|
|
52
|
+
|
|
53
|
+
const Contact = z.object({
|
|
54
|
+
name: z.string(),
|
|
55
|
+
email: z.string().email(),
|
|
56
|
+
phone: z.string().nullable(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const contact = await llm.json('Reach Grace Hopper at grace@navy.mil', { schema: Contact });
|
|
60
|
+
// ^? { name: string; email: string; phone: string | null }
|
|
65
61
|
```
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
immediate `QuotaError` rather than a wasted Shortcuts round trip.
|
|
63
|
+
Refinements the decoder cannot enforce (`.email()`, `.min()`) are checked
|
|
64
|
+
afterwards; a reply that fails gets one automatic retry with the problems
|
|
65
|
+
pointed out, then a `SchemaValidationError` carrying the issues and the raw
|
|
66
|
+
text.
|
|
72
67
|
|
|
73
|
-
|
|
68
|
+
### Streaming
|
|
74
69
|
|
|
75
70
|
```ts
|
|
76
|
-
|
|
71
|
+
for await (const delta of llm.stream('Write a haiku about bridges')) {
|
|
72
|
+
process.stdout.write(delta);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const text = await llm.stream('…'); // or just await the whole reply
|
|
76
|
+
const { usage } = await llm.stream('…').result; // or everything about it
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
Breaking out of the loop cancels generation. Structured output streams too —
|
|
80
|
+
partial objects as the model fills them in, for rendering a card or form while
|
|
81
|
+
it is still being written:
|
|
81
82
|
|
|
82
83
|
```ts
|
|
83
|
-
await llm.
|
|
84
|
+
for await (const partial of llm.streamJson(prompt, { schema: Contact })) {
|
|
85
|
+
render(partial); // { name: 'Grace' } → { name: 'Grace', email: '…' } → …
|
|
86
|
+
}
|
|
84
87
|
```
|
|
85
88
|
|
|
86
|
-
|
|
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:
|
|
89
|
+
### Tools: your JavaScript, called by the model
|
|
91
90
|
|
|
92
91
|
```ts
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
import { tool } from 'apple-llm';
|
|
93
|
+
|
|
94
|
+
const getWeather = tool({
|
|
95
|
+
name: 'getWeather',
|
|
96
|
+
description: 'Current weather for a city',
|
|
97
|
+
parameters: z.object({ city: z.string() }),
|
|
98
|
+
execute: async ({ city }) => fetchWeather(city), // city: string
|
|
96
99
|
});
|
|
100
|
+
|
|
101
|
+
const result = await llm.generate('Do I need an umbrella in Paris?', {
|
|
102
|
+
tools: [getWeather],
|
|
103
|
+
});
|
|
104
|
+
result.text; // "No — it's 21°C and sunny in Paris."
|
|
105
|
+
result.toolCalls; // [{ name: 'getWeather', arguments: { city: 'Paris' }, output: '…' }]
|
|
97
106
|
```
|
|
98
107
|
|
|
99
|
-
|
|
108
|
+
`execute` runs in your process, mid-generation, with your credentials and
|
|
109
|
+
database. Arguments are schema-guaranteed like `json()`. A tool that throws
|
|
110
|
+
ends the call with a `ToolExecutionError` whose `cause` is your error. Tools
|
|
111
|
+
mix freely with Apple's built-ins: `tools: ['ocr', getWeather]`.
|
|
112
|
+
|
|
113
|
+
Leave out `execute` and generation stops at the call instead
|
|
114
|
+
(`finishReason: 'tool-calls'`), so you can run it yourself — send the result
|
|
115
|
+
back as a `tool` message to continue.
|
|
116
|
+
|
|
117
|
+
### Conversations
|
|
118
|
+
|
|
119
|
+
Pass a message list anywhere a prompt goes:
|
|
100
120
|
|
|
101
121
|
```ts
|
|
102
|
-
|
|
122
|
+
import type { ChatMessage } from 'apple-llm';
|
|
123
|
+
|
|
124
|
+
const messages: ChatMessage[] = [
|
|
125
|
+
{ role: 'system', content: 'You are terse.' },
|
|
126
|
+
{ role: 'user', content: 'My cat is called Biscuit.' },
|
|
127
|
+
];
|
|
128
|
+
const reply = await llm.generate(messages);
|
|
129
|
+
messages.push(reply.message, { role: 'user', content: 'What is my cat called?' });
|
|
130
|
+
await llm.text(messages); // "Biscuit."
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
History becomes Apple's native transcript, not a text preamble. When a long
|
|
134
|
+
conversation outgrows the context window, the oldest turns are dropped to fit
|
|
135
|
+
and `result.trimmedTurns` says how many (`trimHistory: false` to get a
|
|
136
|
+
`ContextLengthError` instead).
|
|
103
137
|
|
|
104
|
-
|
|
138
|
+
Or let the helper keep the thread — it survives restarts:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const chat = llm.conversation('support-ticket-42', { system: 'You are terse.' });
|
|
105
142
|
await chat.text('My cat is called Biscuit.');
|
|
106
|
-
await chat.text('What is my cat called?');
|
|
107
|
-
|
|
143
|
+
await chat.text('What is my cat called?'); // "Biscuit."
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Cancellation and timeouts
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
await llm.text(prompt, { signal: controller.signal }); // AbortError
|
|
150
|
+
await llm.text(prompt, { timeoutMs: 5_000 }); // TimeoutError
|
|
151
|
+
new AppleLLM({ timeoutMs: 30_000 }); // default for every call
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Cancelling stops the model itself, not just the promise: the next call does not
|
|
155
|
+
wait behind a reply nobody wants.
|
|
156
|
+
|
|
157
|
+
### Images, documents, long text
|
|
108
158
|
|
|
159
|
+
```ts
|
|
160
|
+
await llm.text('What is in this photo?', { images: ['./photo.jpg'] });
|
|
109
161
|
await llm.text('Read the total.', { images: ['./receipt.png'], tools: ['ocr'] });
|
|
110
|
-
await llm.text('Which notes mention the bridge?', { tools: ['spotlight'] });
|
|
111
162
|
await llm.text('Compare these quotes.', { documents: ['./a.md', './b.md'] });
|
|
163
|
+
await llm.summarize(fiftyPageReport); // longer than the window? summarised in parts
|
|
164
|
+
```
|
|
112
165
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
await llm.
|
|
166
|
+
### Knowing what a call costs
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const { tokens, contextSize } = await llm.countTokens(messages, { schema, tools });
|
|
170
|
+
const { usage, finishReason } = await llm.generate(prompt);
|
|
171
|
+
// usage: { inputTokens, outputTokens, totalTokens, cachedInputTokens }
|
|
172
|
+
// finishReason: 'stop' | 'length' | 'tool-calls' — 'length' means maxTokens cut it off
|
|
117
173
|
```
|
|
118
174
|
|
|
119
|
-
|
|
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):
|
|
175
|
+
### The big model: Private Cloud Compute
|
|
122
176
|
|
|
123
177
|
```ts
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
await
|
|
178
|
+
const cloud = new AppleLLM({ tier: 'cloud' }); // after `npx apple-llm setup-cloud`, once
|
|
179
|
+
|
|
180
|
+
await cloud.text(puzzle); // reasons it through
|
|
181
|
+
await cloud.text('What is in this photo?', { images: ['./photo.jpg'] });
|
|
182
|
+
await cloud.text('Who won the last World Cup?', { webSearch: true }); // live web knowledge
|
|
183
|
+
await cloud.json(prompt, { schema: Contact }); // typed, validated
|
|
127
184
|
```
|
|
128
185
|
|
|
129
|
-
|
|
186
|
+
On macOS 27 Golden Gate this reaches the next-generation server model Apple
|
|
187
|
+
built Siri AI on. `probe()` reads what it can do straight from the framework —
|
|
188
|
+
reasoning, vision, tool calling and a 32,768-token window — without calling it.
|
|
189
|
+
Measured against the on-device model: it solves multi-step logic puzzles the
|
|
190
|
+
small model loses track of, finds a fact buried in 25,000 tokens, reads images,
|
|
191
|
+
and with `webSearch` answers questions about last month's news correctly. It
|
|
192
|
+
costs no API key and no money, only Apple's per-device quota
|
|
193
|
+
(`probe().cloud.quota`), and your prompt leaves the Mac.
|
|
194
|
+
|
|
195
|
+
## OpenAI-compatible server
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npx apple-llm serve
|
|
199
|
+
# apple-llm serving an OpenAI-compatible API at http://127.0.0.1:11436/v1
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Point any OpenAI client at it — the official SDKs, LangChain, LlamaIndex,
|
|
203
|
+
editor plugins, Open WebUI — with any API key:
|
|
130
204
|
|
|
131
205
|
```ts
|
|
132
|
-
import
|
|
206
|
+
import OpenAI from 'openai';
|
|
207
|
+
const openai = new OpenAI({ baseURL: 'http://127.0.0.1:11436/v1', apiKey: 'unused' });
|
|
208
|
+
await openai.chat.completions.create({ model: 'apple-on-device', messages });
|
|
133
209
|
```
|
|
134
210
|
|
|
135
|
-
`
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
|
|
211
|
+
`/v1/chat/completions` supports streaming, tool calls, `response_format` with
|
|
212
|
+
a JSON Schema (guaranteed on device), images as data or http URLs, `seed` and
|
|
213
|
+
`top_p`. It binds to localhost, sends no CORS headers unless you pass
|
|
214
|
+
`--cors <origin>`, can require `--api-key`, and serves the on-device model
|
|
215
|
+
unless a client asks for `apple-private-cloud` by name. Or from code:
|
|
216
|
+
`import { serve } from 'apple-llm/server'`.
|
|
217
|
+
|
|
218
|
+
## Vercel AI SDK
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { generateText, streamText, stepCountIs, tool } from 'ai';
|
|
222
|
+
import { apple } from 'apple-llm/ai-sdk';
|
|
223
|
+
|
|
224
|
+
const { text } = await generateText({
|
|
225
|
+
model: apple(), // on device; apple('cloud') for Private Cloud Compute
|
|
226
|
+
prompt: 'What is the weather in Paris?',
|
|
227
|
+
tools: { getWeather: tool({ inputSchema: z.object({ city: z.string() }), execute: … }) },
|
|
228
|
+
stopWhen: stepCountIs(3),
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
AI SDK 6 and 7. `generateText`, `streamText`, `generateObject` / `Output.object`
|
|
233
|
+
(constrained on device), multi-step tools, images and abort signals. Apple-only
|
|
234
|
+
settings go in `providerOptions: { apple: { useCase, guardrails, builtInTools } }`.
|
|
139
235
|
|
|
140
236
|
## CLI
|
|
141
237
|
|
|
142
238
|
```bash
|
|
143
|
-
apple-llm
|
|
144
|
-
apple-llm
|
|
145
|
-
apple-llm run --
|
|
146
|
-
apple-llm run --
|
|
147
|
-
apple-llm run --
|
|
148
|
-
apple-llm run --image
|
|
239
|
+
apple-llm chat # interactive, streamed; Ctrl+C stops a reply
|
|
240
|
+
apple-llm run "Summarize: …" # one completion; "-" reads stdin
|
|
241
|
+
apple-llm run --json "…" # text + usage + tool calls as JSON
|
|
242
|
+
apple-llm run --schema person.json "Ada, 1815" # guaranteed JSON
|
|
243
|
+
apple-llm run --schema person.json --stream "…" # partial objects, one per line
|
|
244
|
+
apple-llm run --image photo.png "What is this?"
|
|
149
245
|
apple-llm run --tool ocr --image receipt.png "Read the total."
|
|
150
|
-
apple-llm
|
|
151
|
-
apple-llm
|
|
152
|
-
apple-llm
|
|
153
|
-
apple-llm
|
|
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
|
|
246
|
+
apple-llm serve [--port 11436] [--api-key k] # OpenAI-compatible API
|
|
247
|
+
apple-llm count --system "…" - # tokens before sending
|
|
248
|
+
apple-llm probe # what this machine can do
|
|
249
|
+
apple-llm setup-cloud # one-time, for the cloud tier
|
|
159
250
|
```
|
|
160
251
|
|
|
161
|
-
|
|
252
|
+
`apple-llm --help` for everything, including the Write-with-Siri presets
|
|
253
|
+
(`rewrite`, `proofread`, `summarize`, `draft`) and `ask-screen`.
|
|
254
|
+
|
|
255
|
+
## Two traps worth knowing
|
|
256
|
+
|
|
257
|
+
**Never set `temperature: 0`.** Constrained decoding already guarantees the
|
|
258
|
+
schema, so greedy decoding buys nothing and reliably degenerates — it padded an
|
|
259
|
+
unbounded array forever, then ran away inside a single string, turning a 2s
|
|
260
|
+
call into 20s. The default is `0.4`. For reproducible output use a seed
|
|
261
|
+
instead: `sampling: { mode: 'topK', k: 50, seed: 42 }` returns byte-identical
|
|
262
|
+
text across runs, without the degeneration.
|
|
263
|
+
|
|
264
|
+
**Bound your arrays.** Apple honours `maxItems` (`.max(5)` in Zod) but ignores
|
|
265
|
+
`maxLength` on strings.
|
|
266
|
+
|
|
267
|
+
## The two tiers
|
|
268
|
+
|
|
269
|
+
| | on-device | cloud (Private Cloud Compute) |
|
|
270
|
+
|---|---|---|
|
|
271
|
+
| Model | AFM 3 Core (~1–4B active) | Apple's next-gen server model (Siri AI's, on macOS 27) |
|
|
272
|
+
| Privacy | nothing leaves the Mac | **your prompt leaves the Mac** |
|
|
273
|
+
| Context | 8,192 tokens (4,096 on macOS 26) | 32,768 tokens |
|
|
274
|
+
| Reasoning | no | yes |
|
|
275
|
+
| Latency | ~0.3–2s warm | ~2s, ~10s at 25k tokens |
|
|
276
|
+
| JSON | guaranteed by constrained decoding | requested, recovered, validated |
|
|
277
|
+
| Streaming | yes, text and objects | one chunk (Shortcuts is not incremental) |
|
|
278
|
+
| Images | yes (macOS 27) | yes (macOS 27 Golden Gate) |
|
|
279
|
+
| Web knowledge | no | yes, with `webSearch` |
|
|
280
|
+
| Your tools, sessions | yes | no |
|
|
281
|
+
| Setup | none | `apple-llm setup-cloud`, once |
|
|
282
|
+
| Limits | none | Apple's quota (read before calling: `probe().cloud.quota`) |
|
|
283
|
+
|
|
284
|
+
## Errors
|
|
285
|
+
|
|
286
|
+
Every error is an `AppleLLMError` with a stable `code`, and a class to branch on:
|
|
287
|
+
|
|
288
|
+
| class | when |
|
|
289
|
+
|---|---|
|
|
290
|
+
| `ModelUnavailableError` | Apple Intelligence off, model downloading, unsupported Mac/OS (`.reason` says which) |
|
|
291
|
+
| `ContextLengthError` | the request does not fit (`.contextSize`, `.tokenCount`) |
|
|
292
|
+
| `SchemaRejectedError` | Apple's decoder refused a schema |
|
|
293
|
+
| `SchemaValidationError` | the reply failed your schema's validation (`.issues`, `.text`) |
|
|
294
|
+
| `ToolExecutionError` | your tool threw (`.toolName`, `.cause`) |
|
|
295
|
+
| `AbortError` / `TimeoutError` | your signal fired / the deadline passed |
|
|
296
|
+
| `QuotaError` | rate limited (`.resetDate` when Apple gives one) |
|
|
297
|
+
| `RefusalError` | the model's guardrails declined |
|
|
298
|
+
| `ModelBusyError` | other processes are using the model; already retried with backoff |
|
|
299
|
+
| `SetupRequiredError`, `UnsupportedError` | a setup step is missing / the tier cannot do that |
|
|
300
|
+
|
|
301
|
+
## Resource use
|
|
162
302
|
|
|
163
|
-
`
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
303
|
+
One `AppleLLM` keeps one helper process warm — construct it once. The helper
|
|
304
|
+
does not keep your process alive, so a script exits without `close()`; call
|
|
305
|
+
`close()` (or use `await using llm = new AppleLLM()`) to release it early in a
|
|
306
|
+
long-running program. Apple serialises inference, so requests queue rather than
|
|
307
|
+
run in parallel — concurrency buys nothing here.
|
|
168
308
|
|
|
169
|
-
|
|
170
|
-
mistake invisible but total. A union collapsed to the wrong branch does not warn
|
|
171
|
-
— it makes the right answer unreachable.
|
|
309
|
+
## More
|
|
172
310
|
|
|
173
|
-
|
|
311
|
+
The [repository README](https://github.com/jagdishpal02000/apple-llm#readme)
|
|
312
|
+
has the measurements behind every default, the eight rules of Apple's schema
|
|
313
|
+
dialect, and why the cloud tier goes through Shortcuts.
|
|
174
314
|
|
|
175
|
-
|
|
176
|
-
both routes were discovered and shipped. MIT.
|
|
315
|
+
MIT.
|