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/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AbortError,
|
|
2
3
|
AppleLLM,
|
|
3
4
|
AppleLLMError,
|
|
4
5
|
CLOUD_CONTEXT_TOKENS,
|
|
@@ -8,14 +9,21 @@ import {
|
|
|
8
9
|
ContextLengthError,
|
|
9
10
|
Conversation,
|
|
10
11
|
DEFAULT_MAX_TOKENS,
|
|
12
|
+
DEFAULT_MAX_TOOL_CALLS,
|
|
11
13
|
DEFAULT_TEMPERATURE,
|
|
12
14
|
DeviceClient,
|
|
15
|
+
ModelBusyError,
|
|
13
16
|
ModelUnavailableError,
|
|
14
17
|
QuotaError,
|
|
15
18
|
RefusalError,
|
|
19
|
+
ReplayBook,
|
|
20
|
+
ResultStream,
|
|
16
21
|
SchemaRejectedError,
|
|
22
|
+
SchemaValidationError,
|
|
17
23
|
SetupRequiredError,
|
|
18
24
|
TimeoutError,
|
|
25
|
+
ToolExecutionError,
|
|
26
|
+
UnsupportedError,
|
|
19
27
|
assertTools,
|
|
20
28
|
cacheDir,
|
|
21
29
|
captureScreenshot,
|
|
@@ -27,18 +35,26 @@ import {
|
|
|
27
35
|
hostTarget,
|
|
28
36
|
installCloudShortcut,
|
|
29
37
|
isAppleSiliconMac,
|
|
38
|
+
isStandardSchema,
|
|
39
|
+
normalizeTools,
|
|
30
40
|
parseImageFlag,
|
|
31
41
|
parseLlmJson,
|
|
32
42
|
probe,
|
|
33
43
|
probeCloud,
|
|
34
44
|
probeDevice,
|
|
45
|
+
resolveSchema,
|
|
46
|
+
restoreNulls,
|
|
35
47
|
shortcutDefinition,
|
|
48
|
+
splitMessages,
|
|
36
49
|
stripCodeFences,
|
|
37
50
|
targetTripleFrom,
|
|
38
51
|
toAppleSchema,
|
|
52
|
+
tool,
|
|
53
|
+
toolOutputText,
|
|
39
54
|
withDocuments
|
|
40
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-GM325EMJ.js";
|
|
41
56
|
export {
|
|
57
|
+
AbortError,
|
|
42
58
|
AppleLLM,
|
|
43
59
|
AppleLLMError,
|
|
44
60
|
CLOUD_CONTEXT_TOKENS,
|
|
@@ -48,14 +64,21 @@ export {
|
|
|
48
64
|
ContextLengthError,
|
|
49
65
|
Conversation,
|
|
50
66
|
DEFAULT_MAX_TOKENS,
|
|
67
|
+
DEFAULT_MAX_TOOL_CALLS,
|
|
51
68
|
DEFAULT_TEMPERATURE,
|
|
52
69
|
DeviceClient,
|
|
70
|
+
ModelBusyError,
|
|
53
71
|
ModelUnavailableError,
|
|
54
72
|
QuotaError,
|
|
55
73
|
RefusalError,
|
|
74
|
+
ReplayBook,
|
|
75
|
+
ResultStream,
|
|
56
76
|
SchemaRejectedError,
|
|
77
|
+
SchemaValidationError,
|
|
57
78
|
SetupRequiredError,
|
|
58
79
|
TimeoutError,
|
|
80
|
+
ToolExecutionError,
|
|
81
|
+
UnsupportedError,
|
|
59
82
|
assertTools,
|
|
60
83
|
cacheDir,
|
|
61
84
|
captureScreenshot,
|
|
@@ -67,14 +90,21 @@ export {
|
|
|
67
90
|
hostTarget,
|
|
68
91
|
installCloudShortcut,
|
|
69
92
|
isAppleSiliconMac,
|
|
93
|
+
isStandardSchema,
|
|
94
|
+
normalizeTools,
|
|
70
95
|
parseImageFlag,
|
|
71
96
|
parseLlmJson,
|
|
72
97
|
probe,
|
|
73
98
|
probeCloud,
|
|
74
99
|
probeDevice,
|
|
100
|
+
resolveSchema,
|
|
101
|
+
restoreNulls,
|
|
75
102
|
shortcutDefinition,
|
|
103
|
+
splitMessages,
|
|
76
104
|
stripCodeFences,
|
|
77
105
|
targetTripleFrom,
|
|
78
106
|
toAppleSchema,
|
|
107
|
+
tool,
|
|
108
|
+
toolOutputText,
|
|
79
109
|
withDocuments
|
|
80
110
|
};
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
var _chunkOQATUZWFcjs = require('./chunk-OQATUZWF.cjs');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
var _chunkNRDZIP5Gcjs = require('./chunk-NRDZIP5G.cjs');
|
|
21
|
+
|
|
22
|
+
// src/server.ts
|
|
23
|
+
var _http = require('http'); var _http2 = _interopRequireDefault(_http);
|
|
24
|
+
var _crypto = require('crypto');
|
|
25
|
+
var DEFAULT_PORT = 11436;
|
|
26
|
+
var MODELS = {
|
|
27
|
+
"apple-on-device": "device",
|
|
28
|
+
"apple-private-cloud": "cloud"
|
|
29
|
+
};
|
|
30
|
+
function errorResponse(error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
const make = (status, type, code) => ({
|
|
33
|
+
status,
|
|
34
|
+
body: { error: { message, type, code: _nullishCoalesce(code, () => ( null)), param: null } }
|
|
35
|
+
});
|
|
36
|
+
if (error instanceof _chunkNRDZIP5Gcjs.ContextLengthError) return make(400, "invalid_request_error", "context_length_exceeded");
|
|
37
|
+
if (error instanceof _chunkNRDZIP5Gcjs.SchemaRejectedError || error instanceof _chunkNRDZIP5Gcjs.SchemaValidationError) {
|
|
38
|
+
return make(400, "invalid_request_error", "invalid_schema");
|
|
39
|
+
}
|
|
40
|
+
if (error instanceof _chunkNRDZIP5Gcjs.UnsupportedError) return make(400, "invalid_request_error", "unsupported");
|
|
41
|
+
if (error instanceof _chunkNRDZIP5Gcjs.RefusalError) return make(400, "invalid_request_error", "content_filter");
|
|
42
|
+
if (error instanceof _chunkNRDZIP5Gcjs.QuotaError) return make(429, "rate_limit_error", "quota_exceeded");
|
|
43
|
+
if (error instanceof _chunkNRDZIP5Gcjs.TimeoutError) return make(504, "timeout_error");
|
|
44
|
+
if (error instanceof _chunkNRDZIP5Gcjs.AbortError) return make(499, "request_cancelled");
|
|
45
|
+
if (error instanceof _chunkNRDZIP5Gcjs.ModelBusyError) return make(503, "service_unavailable", "model_busy");
|
|
46
|
+
if (error instanceof _chunkNRDZIP5Gcjs.ModelUnavailableError || error instanceof _chunkNRDZIP5Gcjs.SetupRequiredError) {
|
|
47
|
+
return make(503, "service_unavailable", "model_unavailable");
|
|
48
|
+
}
|
|
49
|
+
if (error instanceof HttpError) return make(error.status, "invalid_request_error");
|
|
50
|
+
if (error instanceof _chunkNRDZIP5Gcjs.AppleLLMError) return make(500, "server_error");
|
|
51
|
+
return make(500, "server_error");
|
|
52
|
+
}
|
|
53
|
+
var HttpError = class extends Error {
|
|
54
|
+
constructor(status, message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.status = status;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
};
|
|
60
|
+
function textOf(content) {
|
|
61
|
+
if (typeof content === "string") return content;
|
|
62
|
+
if (content === null || content === void 0) return "";
|
|
63
|
+
if (Array.isArray(content)) {
|
|
64
|
+
return content.filter((p) => p.type === "text" || p.type === "input_text").map((p) => _nullishCoalesce(p.text, () => ( ""))).join("\n");
|
|
65
|
+
}
|
|
66
|
+
return String(content);
|
|
67
|
+
}
|
|
68
|
+
async function toMessages(raw, images, signal) {
|
|
69
|
+
if (!Array.isArray(raw) || raw.length === 0) throw new HttpError(400, "`messages` must be a non-empty array.");
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const message of raw) {
|
|
72
|
+
switch (message.role) {
|
|
73
|
+
case "system":
|
|
74
|
+
case "developer":
|
|
75
|
+
out.push({ role: "system", content: textOf(message.content) });
|
|
76
|
+
break;
|
|
77
|
+
case "user": {
|
|
78
|
+
const paths = [];
|
|
79
|
+
if (Array.isArray(message.content)) {
|
|
80
|
+
for (const part of message.content) {
|
|
81
|
+
if (part.type === "image_url" && part.image_url !== void 0) {
|
|
82
|
+
const url = typeof part.image_url === "string" ? part.image_url : part.image_url.url;
|
|
83
|
+
paths.push(await images.add(url, void 0, signal));
|
|
84
|
+
} else if (part.type !== "text" && part.type !== "input_text") {
|
|
85
|
+
throw new HttpError(400, `Content part "${part.type}" is not supported.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
out.push({ role: "user", content: textOf(message.content), ...paths.length > 0 ? { images: paths } : {} });
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "assistant":
|
|
93
|
+
out.push({
|
|
94
|
+
role: "assistant",
|
|
95
|
+
content: textOf(message.content),
|
|
96
|
+
...message.tool_calls !== void 0 && message.tool_calls.length > 0 ? {
|
|
97
|
+
toolCalls: message.tool_calls.map((call) => ({
|
|
98
|
+
id: call.id,
|
|
99
|
+
name: call.function.name,
|
|
100
|
+
arguments: parseArguments(call.function.arguments)
|
|
101
|
+
}))
|
|
102
|
+
} : {}
|
|
103
|
+
});
|
|
104
|
+
break;
|
|
105
|
+
case "tool":
|
|
106
|
+
out.push({ role: "tool", toolCallId: String(_nullishCoalesce(message.tool_call_id, () => ( ""))), name: message.name, content: textOf(message.content) });
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
throw new HttpError(400, `Message role "${message.role}" is not supported.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function parseArguments(text) {
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(text);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function openAIToolCalls(calls) {
|
|
122
|
+
return calls.filter((call) => call.output === void 0).map((call, index) => ({
|
|
123
|
+
index,
|
|
124
|
+
id: call.id,
|
|
125
|
+
type: "function",
|
|
126
|
+
function: { name: call.name, arguments: JSON.stringify(_nullishCoalesce(call.arguments, () => ( {}))) }
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
function finishReason(result) {
|
|
130
|
+
return result.finishReason === "tool-calls" ? "tool_calls" : result.finishReason;
|
|
131
|
+
}
|
|
132
|
+
function usageOf(result) {
|
|
133
|
+
if (result.usage === void 0) return void 0;
|
|
134
|
+
return {
|
|
135
|
+
prompt_tokens: result.usage.inputTokens,
|
|
136
|
+
completion_tokens: result.usage.outputTokens,
|
|
137
|
+
total_tokens: result.usage.totalTokens
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function readBody(req, limit = 32 * 1024 * 1024) {
|
|
141
|
+
const chunks = [];
|
|
142
|
+
let size = 0;
|
|
143
|
+
for await (const chunk of req) {
|
|
144
|
+
size += chunk.length;
|
|
145
|
+
if (size > limit) throw new HttpError(413, "Request body too large.");
|
|
146
|
+
chunks.push(chunk);
|
|
147
|
+
}
|
|
148
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
149
|
+
if (text === "") return {};
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(text);
|
|
152
|
+
} catch (e2) {
|
|
153
|
+
throw new HttpError(400, "Request body is not valid JSON.");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function createServer(options = {}) {
|
|
157
|
+
const { port: _port, host: _host, tier: defaultTier = "device", apiKey, cors, log, ...llmOptions } = options;
|
|
158
|
+
const clients = /* @__PURE__ */ new Map();
|
|
159
|
+
const clientFor = (tier) => {
|
|
160
|
+
let client = clients.get(tier);
|
|
161
|
+
if (client === void 0) {
|
|
162
|
+
client = new (0, _chunkNRDZIP5Gcjs.AppleLLM)({ ...llmOptions, tier });
|
|
163
|
+
clients.set(tier, client);
|
|
164
|
+
}
|
|
165
|
+
return client;
|
|
166
|
+
};
|
|
167
|
+
const server = _http2.default.createServer((req, res) => {
|
|
168
|
+
const started = Date.now();
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
res.on("close", () => {
|
|
171
|
+
if (!res.writableFinished) controller.abort();
|
|
172
|
+
});
|
|
173
|
+
const send = (status, body) => {
|
|
174
|
+
if (res.headersSent) return;
|
|
175
|
+
res.writeHead(status, { "content-type": "application/json", ...corsHeaders() });
|
|
176
|
+
res.end(JSON.stringify(body));
|
|
177
|
+
};
|
|
178
|
+
const corsHeaders = () => cors === void 0 ? {} : {
|
|
179
|
+
"access-control-allow-origin": cors,
|
|
180
|
+
"access-control-allow-headers": "authorization, content-type",
|
|
181
|
+
"access-control-allow-methods": "GET, POST, OPTIONS"
|
|
182
|
+
};
|
|
183
|
+
const route = async () => {
|
|
184
|
+
const url = new URL(_nullishCoalesce(req.url, () => ( "/")), "http://localhost");
|
|
185
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
186
|
+
if (req.method === "OPTIONS") {
|
|
187
|
+
res.writeHead(204, corsHeaders());
|
|
188
|
+
res.end();
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
if (apiKey !== void 0 && req.headers.authorization !== `Bearer ${apiKey}`) {
|
|
192
|
+
send(401, { error: { message: "Invalid API key.", type: "invalid_request_error", code: "invalid_api_key", param: null } });
|
|
193
|
+
return void 0;
|
|
194
|
+
}
|
|
195
|
+
if (req.method === "GET" && (pathname === "/health" || pathname === "/v1/health")) {
|
|
196
|
+
const state = await _chunkNRDZIP5Gcjs.probe.call(void 0, );
|
|
197
|
+
send(state.device.available || state.cloud.available ? 200 : 503, {
|
|
198
|
+
status: state.device.available || state.cloud.available ? "ok" : "unavailable",
|
|
199
|
+
device: { available: state.device.available, variant: state.device.variant, contextSize: state.device.contextSize, reason: state.device.reason },
|
|
200
|
+
cloud: { available: state.cloud.available, quota: _optionalChain([state, 'access', _ => _.cloud, 'access', _2 => _2.quota, 'optionalAccess', _3 => _3.status]), reason: state.cloud.reason }
|
|
201
|
+
});
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
204
|
+
if (req.method === "GET" && pathname === "/v1/models") {
|
|
205
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
206
|
+
send(200, {
|
|
207
|
+
object: "list",
|
|
208
|
+
data: Object.keys(MODELS).map((id) => ({ id, object: "model", created, owned_by: "apple" }))
|
|
209
|
+
});
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
if (req.method === "POST" && pathname === "/v1/chat/completions") {
|
|
213
|
+
const body = await readBody(req);
|
|
214
|
+
return chatCompletions(body);
|
|
215
|
+
}
|
|
216
|
+
send(404, { error: { message: `No route for ${req.method} ${pathname}.`, type: "invalid_request_error", code: "not_found", param: null } });
|
|
217
|
+
return void 0;
|
|
218
|
+
};
|
|
219
|
+
const chatCompletions = async (body) => {
|
|
220
|
+
const requested = _nullishCoalesce(body.model, () => ( ""));
|
|
221
|
+
const tier = requested in MODELS ? MODELS[requested] : defaultTier;
|
|
222
|
+
const model = requested in MODELS ? requested : tier === "cloud" ? "apple-private-cloud" : "apple-on-device";
|
|
223
|
+
if (body.n !== void 0 && body.n !== 1) throw new HttpError(400, "Only n=1 is supported.");
|
|
224
|
+
const images = new (0, _chunkOQATUZWFcjs.TempImages)();
|
|
225
|
+
try {
|
|
226
|
+
const messages = await toMessages(body.messages, images, controller.signal);
|
|
227
|
+
const functions = [];
|
|
228
|
+
if (body.tool_choice !== "none") {
|
|
229
|
+
for (const def of _nullishCoalesce(body.tools, () => ( []))) {
|
|
230
|
+
if (def.type !== "function" || def.function === void 0) continue;
|
|
231
|
+
functions.push(_chunkNRDZIP5Gcjs.tool.call(void 0, { name: def.function.name, description: def.function.description, parameters: def.function.parameters }));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
let schema;
|
|
235
|
+
if (_optionalChain([body, 'access', _4 => _4.response_format, 'optionalAccess', _5 => _5.type]) === "json_schema" && _optionalChain([body, 'access', _6 => _6.response_format, 'access', _7 => _7.json_schema, 'optionalAccess', _8 => _8.schema]) !== void 0) {
|
|
236
|
+
schema = body.response_format.json_schema.schema;
|
|
237
|
+
} else if (_optionalChain([body, 'access', _9 => _9.response_format, 'optionalAccess', _10 => _10.type]) === "json_object") {
|
|
238
|
+
messages.unshift({ role: "system", content: "Reply with a single JSON object and nothing else." });
|
|
239
|
+
}
|
|
240
|
+
let sampling;
|
|
241
|
+
if (body.top_p !== void 0) sampling = { mode: "threshold", p: body.top_p, seed: body.seed };
|
|
242
|
+
else if (body.seed !== void 0) sampling = { mode: "topK", k: 50, seed: body.seed };
|
|
243
|
+
const call = {
|
|
244
|
+
temperature: body.temperature,
|
|
245
|
+
maxTokens: _nullishCoalesce(body.max_completion_tokens, () => ( body.max_tokens)),
|
|
246
|
+
sampling,
|
|
247
|
+
tools: functions.length > 0 ? functions : void 0,
|
|
248
|
+
signal: controller.signal
|
|
249
|
+
};
|
|
250
|
+
const client = clientFor(tier);
|
|
251
|
+
const id = `chatcmpl-${_crypto.randomUUID.call(void 0, ).replace(/-/g, "").slice(0, 24)}`;
|
|
252
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
253
|
+
if (body.stream === true) {
|
|
254
|
+
res.writeHead(200, {
|
|
255
|
+
"content-type": "text/event-stream",
|
|
256
|
+
"cache-control": "no-cache",
|
|
257
|
+
connection: "keep-alive",
|
|
258
|
+
...corsHeaders()
|
|
259
|
+
});
|
|
260
|
+
const event = (payload) => {
|
|
261
|
+
if (!res.destroyed) res.write(`data: ${typeof payload === "string" ? payload : JSON.stringify(payload)}
|
|
262
|
+
|
|
263
|
+
`);
|
|
264
|
+
};
|
|
265
|
+
const chunk = (delta, finish = null) => {
|
|
266
|
+
event({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta, finish_reason: finish, logprobs: null }] });
|
|
267
|
+
};
|
|
268
|
+
chunk({ role: "assistant", content: "" });
|
|
269
|
+
try {
|
|
270
|
+
let result2;
|
|
271
|
+
if (schema === void 0) {
|
|
272
|
+
const stream = client.stream(messages, call);
|
|
273
|
+
for await (const delta of stream) chunk({ content: delta });
|
|
274
|
+
result2 = await stream.result;
|
|
275
|
+
} else {
|
|
276
|
+
result2 = await client.generate(messages, { ...call, schema });
|
|
277
|
+
if (result2.finishReason !== "tool-calls") chunk({ content: result2.text });
|
|
278
|
+
}
|
|
279
|
+
const calls2 = openAIToolCalls(result2.toolCalls);
|
|
280
|
+
if (calls2.length > 0 && result2.finishReason === "tool-calls") chunk({ tool_calls: calls2 });
|
|
281
|
+
chunk({}, finishReason(result2));
|
|
282
|
+
if (_optionalChain([body, 'access', _11 => _11.stream_options, 'optionalAccess', _12 => _12.include_usage]) === true) {
|
|
283
|
+
event({ id, object: "chat.completion.chunk", created, model, choices: [], usage: _nullishCoalesce(usageOf(result2), () => ( null)) });
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
event(errorResponse(error).body);
|
|
287
|
+
if (!res.destroyed) res.end("data: [DONE]\n\n");
|
|
288
|
+
return `${model} stream failed: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`;
|
|
289
|
+
}
|
|
290
|
+
if (!res.destroyed) res.end("data: [DONE]\n\n");
|
|
291
|
+
return `${model} stream`;
|
|
292
|
+
}
|
|
293
|
+
const result = schema === void 0 ? await client.generate(messages, call) : await client.generate(messages, { ...call, schema });
|
|
294
|
+
const calls = result.finishReason === "tool-calls" ? openAIToolCalls(result.toolCalls) : [];
|
|
295
|
+
send(200, {
|
|
296
|
+
id,
|
|
297
|
+
object: "chat.completion",
|
|
298
|
+
created,
|
|
299
|
+
model,
|
|
300
|
+
choices: [
|
|
301
|
+
{
|
|
302
|
+
index: 0,
|
|
303
|
+
message: {
|
|
304
|
+
role: "assistant",
|
|
305
|
+
content: result.finishReason === "tool-calls" ? null : result.text,
|
|
306
|
+
...calls.length > 0 ? { tool_calls: calls.map(({ index: _i, ...rest }) => rest) } : {},
|
|
307
|
+
refusal: null
|
|
308
|
+
},
|
|
309
|
+
finish_reason: finishReason(result),
|
|
310
|
+
logprobs: null
|
|
311
|
+
}
|
|
312
|
+
],
|
|
313
|
+
usage: _nullishCoalesce(usageOf(result), () => ( { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }))
|
|
314
|
+
});
|
|
315
|
+
return model;
|
|
316
|
+
} finally {
|
|
317
|
+
await images.dispose();
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
route().then((what) => {
|
|
321
|
+
if (what !== void 0) _optionalChain([log, 'optionalCall', _13 => _13(`${req.method} ${req.url} ${what} ${res.statusCode} ${Date.now() - started}ms`)]);
|
|
322
|
+
}).catch((error) => {
|
|
323
|
+
const { status, body } = errorResponse(error);
|
|
324
|
+
if (error instanceof _chunkNRDZIP5Gcjs.ModelBusyError && !res.headersSent) res.setHeader("retry-after", "2");
|
|
325
|
+
send(status, body);
|
|
326
|
+
_optionalChain([log, 'optionalCall', _14 => _14(`${req.method} ${req.url} ${status} ${Date.now() - started}ms ${error instanceof Error ? error.message.split("\n")[0] : ""}`)]);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
server.on("close", () => {
|
|
330
|
+
for (const client of clients.values()) client.close();
|
|
331
|
+
});
|
|
332
|
+
return server;
|
|
333
|
+
}
|
|
334
|
+
async function serve(options = {}) {
|
|
335
|
+
const server = createServer(options);
|
|
336
|
+
const port = _nullishCoalesce(options.port, () => ( DEFAULT_PORT));
|
|
337
|
+
const host = _nullishCoalesce(options.host, () => ( "127.0.0.1"));
|
|
338
|
+
await new Promise((resolve, reject) => {
|
|
339
|
+
server.once("error", reject);
|
|
340
|
+
server.listen(port, host, () => {
|
|
341
|
+
server.off("error", reject);
|
|
342
|
+
resolve();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
const address = server.address();
|
|
346
|
+
const bound = typeof address === "object" && address !== null ? address.port : port;
|
|
347
|
+
return {
|
|
348
|
+
server,
|
|
349
|
+
url: `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${bound}/v1`,
|
|
350
|
+
close: () => new Promise((resolve) => {
|
|
351
|
+
server.close(() => resolve());
|
|
352
|
+
_optionalChain([server, 'access', _15 => _15.closeAllConnections, 'optionalCall', _16 => _16()]);
|
|
353
|
+
})
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
exports.DEFAULT_PORT = DEFAULT_PORT; exports.MODELS = MODELS; exports.createServer = createServer; exports.serve = serve;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { A as AppleLLMOptions } from './client-CXewzZTj.cjs';
|
|
3
|
+
import 'node:child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An OpenAI-compatible HTTP server for Apple's models.
|
|
7
|
+
*
|
|
8
|
+
* apple-llm serve # http://127.0.0.1:11436/v1
|
|
9
|
+
*
|
|
10
|
+
* Point any OpenAI client at it — the official SDKs, LangChain, LlamaIndex,
|
|
11
|
+
* editor plugins, Open WebUI — with any API key, and it talks to the on-device
|
|
12
|
+
* model. Implements `POST /v1/chat/completions` (streaming, tools,
|
|
13
|
+
* `response_format` with JSON Schema, images), `GET /v1/models` and
|
|
14
|
+
* `GET /health`.
|
|
15
|
+
*
|
|
16
|
+
* Safe defaults, because a local model server is reachable by more than you
|
|
17
|
+
* might think: it binds to 127.0.0.1 only; CORS is off, so a web page you
|
|
18
|
+
* happen to visit cannot use your model; an API key can be required; and the
|
|
19
|
+
* default tier is the device, so nothing leaves the machine unless a client
|
|
20
|
+
* asks for the `apple-private-cloud` model by name.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
declare const DEFAULT_PORT = 11436;
|
|
24
|
+
/** Model ids a client can ask for. Anything else gets the server's default tier. */
|
|
25
|
+
declare const MODELS: {
|
|
26
|
+
readonly 'apple-on-device': "device";
|
|
27
|
+
readonly 'apple-private-cloud': "cloud";
|
|
28
|
+
};
|
|
29
|
+
interface ServerOptions extends Omit<AppleLLMOptions, 'tier'> {
|
|
30
|
+
port?: number;
|
|
31
|
+
/** Interface to bind. Default 127.0.0.1; use 0.0.0.0 only on a network you trust. */
|
|
32
|
+
host?: string;
|
|
33
|
+
/** Tier for requests that do not name an Apple model. Default `device`. */
|
|
34
|
+
tier?: 'device' | 'cloud' | 'auto';
|
|
35
|
+
/** Require `Authorization: Bearer <apiKey>`. */
|
|
36
|
+
apiKey?: string;
|
|
37
|
+
/** `Access-Control-Allow-Origin` value, for browser clients. Off by default. */
|
|
38
|
+
cors?: string;
|
|
39
|
+
/** One line per request. Default: none. */
|
|
40
|
+
log?: (line: string) => void;
|
|
41
|
+
}
|
|
42
|
+
/** Build the server without listening. `serve()` is the one-liner. */
|
|
43
|
+
declare function createServer(options?: ServerOptions): http.Server;
|
|
44
|
+
/** Start listening. Resolves once the port is bound. */
|
|
45
|
+
declare function serve(options?: ServerOptions): Promise<{
|
|
46
|
+
server: http.Server;
|
|
47
|
+
url: string;
|
|
48
|
+
close: () => Promise<void>;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export { DEFAULT_PORT, MODELS, type ServerOptions, createServer, serve };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { A as AppleLLMOptions } from './client-CXewzZTj.js';
|
|
3
|
+
import 'node:child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An OpenAI-compatible HTTP server for Apple's models.
|
|
7
|
+
*
|
|
8
|
+
* apple-llm serve # http://127.0.0.1:11436/v1
|
|
9
|
+
*
|
|
10
|
+
* Point any OpenAI client at it — the official SDKs, LangChain, LlamaIndex,
|
|
11
|
+
* editor plugins, Open WebUI — with any API key, and it talks to the on-device
|
|
12
|
+
* model. Implements `POST /v1/chat/completions` (streaming, tools,
|
|
13
|
+
* `response_format` with JSON Schema, images), `GET /v1/models` and
|
|
14
|
+
* `GET /health`.
|
|
15
|
+
*
|
|
16
|
+
* Safe defaults, because a local model server is reachable by more than you
|
|
17
|
+
* might think: it binds to 127.0.0.1 only; CORS is off, so a web page you
|
|
18
|
+
* happen to visit cannot use your model; an API key can be required; and the
|
|
19
|
+
* default tier is the device, so nothing leaves the machine unless a client
|
|
20
|
+
* asks for the `apple-private-cloud` model by name.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
declare const DEFAULT_PORT = 11436;
|
|
24
|
+
/** Model ids a client can ask for. Anything else gets the server's default tier. */
|
|
25
|
+
declare const MODELS: {
|
|
26
|
+
readonly 'apple-on-device': "device";
|
|
27
|
+
readonly 'apple-private-cloud': "cloud";
|
|
28
|
+
};
|
|
29
|
+
interface ServerOptions extends Omit<AppleLLMOptions, 'tier'> {
|
|
30
|
+
port?: number;
|
|
31
|
+
/** Interface to bind. Default 127.0.0.1; use 0.0.0.0 only on a network you trust. */
|
|
32
|
+
host?: string;
|
|
33
|
+
/** Tier for requests that do not name an Apple model. Default `device`. */
|
|
34
|
+
tier?: 'device' | 'cloud' | 'auto';
|
|
35
|
+
/** Require `Authorization: Bearer <apiKey>`. */
|
|
36
|
+
apiKey?: string;
|
|
37
|
+
/** `Access-Control-Allow-Origin` value, for browser clients. Off by default. */
|
|
38
|
+
cors?: string;
|
|
39
|
+
/** One line per request. Default: none. */
|
|
40
|
+
log?: (line: string) => void;
|
|
41
|
+
}
|
|
42
|
+
/** Build the server without listening. `serve()` is the one-liner. */
|
|
43
|
+
declare function createServer(options?: ServerOptions): http.Server;
|
|
44
|
+
/** Start listening. Resolves once the port is bound. */
|
|
45
|
+
declare function serve(options?: ServerOptions): Promise<{
|
|
46
|
+
server: http.Server;
|
|
47
|
+
url: string;
|
|
48
|
+
close: () => Promise<void>;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export { DEFAULT_PORT, MODELS, type ServerOptions, createServer, serve };
|