@foldedspacelabs/metistry-mcp-apple-fm 0.7.1 → 0.8.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/README.md +142 -0
- package/dist/helper.d.ts +33 -4
- package/dist/helper.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +60 -6
- package/dist/index.js.map +1 -1
- package/dist/v1.d.ts +62 -0
- package/dist/v1.js +167 -0
- package/dist/v1.js.map +1 -0
- package/manifest.yaml +6 -1
- package/package.json +5 -4
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# `@foldedspacelabs/metistry-mcp-apple-fm`
|
|
2
|
+
|
|
3
|
+
Apple Foundation Models — the on-device model macOS keeps resident — behind
|
|
4
|
+
two surfaces on one loopback listener:
|
|
5
|
+
|
|
6
|
+
| | |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| `GET /v1/models`, `POST /v1/chat/completions` | an **OpenAI-compatible provider**, so any client that already speaks `/v1` can use the model with `response_format: json_schema` |
|
|
9
|
+
| `GET /check`, `POST /classify` | the Metistry bridge contract — a behavioural health probe and a batch classify route |
|
|
10
|
+
|
|
11
|
+
**Cost 0, and ~0 extra resident memory.** The weights belong to Apple
|
|
12
|
+
Intelligence and are in RAM for the operating system whether this runs or
|
|
13
|
+
not. The bridge's own footprint is about 23 MB.
|
|
14
|
+
|
|
15
|
+
Requires an Apple Silicon Mac, macOS 26+, and Apple Intelligence switched on.
|
|
16
|
+
No TCC grants at all.
|
|
17
|
+
|
|
18
|
+
## Standalone
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
export METISTRY_BRIDGE_TOKEN_APPLE_FM=$(openssl rand -base64 32)
|
|
22
|
+
npx @foldedspacelabs/metistry-mcp-apple-fm
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
apple-fm bridge on 127.0.0.1:7810 (helper: …/afm-helper.app/Contents/MacOS/afm-helper)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
curl -sS http://127.0.0.1:7810/v1/models -H "authorization: Bearer $METISTRY_BRIDGE_TOKEN_APPLE_FM"
|
|
31
|
+
# {"object":"list","data":[{"id":"foundation-model","object":"model","created":…,"owned_by":"apple"}]}
|
|
32
|
+
|
|
33
|
+
curl -sS http://127.0.0.1:7810/v1/chat/completions \
|
|
34
|
+
-H "authorization: Bearer $METISTRY_BRIDGE_TOKEN_APPLE_FM" \
|
|
35
|
+
-H 'content-type: application/json' -d '{
|
|
36
|
+
"model":"foundation-model",
|
|
37
|
+
"messages":[{"role":"system","content":"You classify short personal-inbox capture items."},
|
|
38
|
+
{"role":"user","content":"grocery: pick up milk and bread tomorrow before 6pm"}],
|
|
39
|
+
"response_format":{"type":"json_schema","json_schema":{"name":"classification","strict":true,"schema":{
|
|
40
|
+
"type":"object",
|
|
41
|
+
"properties":{"category":{"type":"string","enum":["todo","event","idea","link","note"]},
|
|
42
|
+
"has_action":{"type":"boolean"},
|
|
43
|
+
"action":{"type":"string"}},
|
|
44
|
+
"required":["category","has_action","action"],"additionalProperties":false}}}}'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{"id":"chatcmpl-…","object":"chat.completion","created":…,"model":"foundation-model",
|
|
49
|
+
"choices":[{"index":0,"message":{"role":"assistant",
|
|
50
|
+
"content":"{\"action\": \"Go to grocery store and pick up milk and bread before 6pm tomorrow\", \"has_action\": true, \"category\": \"todo\"}"},
|
|
51
|
+
"finish_reason":"stop"}],
|
|
52
|
+
"usage":{"prompt_tokens":212,"completion_tokens":42,"total_tokens":254},
|
|
53
|
+
"x_metistry":{"cost_usd":0,"schema_mode":"dynamic","respond_ms":739.5}}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| variable | | |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `METISTRY_BRIDGE_TOKEN_APPLE_FM` | **required** | the bearer every route takes |
|
|
59
|
+
| `METISTRY_AFM_PORT` | `7810` | |
|
|
60
|
+
| `METISTRY_AFM_HOST` | `127.0.0.1` | |
|
|
61
|
+
| `METISTRY_AFM_HELPER` | the bundled `afm-helper.app` | path to the Swift helper |
|
|
62
|
+
|
|
63
|
+
## Authentication is not optional
|
|
64
|
+
|
|
65
|
+
Every route takes the bearer, `/v1` included. The network is not a trust
|
|
66
|
+
boundary and loopback is network: an unauthenticated completion endpoint on
|
|
67
|
+
`127.0.0.1` is readable by anything else on the machine, and (without the
|
|
68
|
+
CORS answer below) by any web page you happen to visit. Missing, wrong or
|
|
69
|
+
malformed credentials all get the same uniform 401.
|
|
70
|
+
|
|
71
|
+
## Structured output
|
|
72
|
+
|
|
73
|
+
`response_format: { type: json_schema, … }` is the reason this surface
|
|
74
|
+
exists. The caller's schema is translated **per request** into Apple's
|
|
75
|
+
`DynamicGenerationSchema` and enforced at generation time — no schema is
|
|
76
|
+
compiled into the binary.
|
|
77
|
+
|
|
78
|
+
Supported: `object` (`properties`, `required`), `string` (with a string
|
|
79
|
+
`enum`), `integer`, `number`, `boolean`, `null` (macOS 26.4+), `array`
|
|
80
|
+
(`items`, `minItems`, `maxItems`), and `anyOf` — including `anyOf` over
|
|
81
|
+
object subschemas, i.e. a discriminated union assembled at request time.
|
|
82
|
+
|
|
83
|
+
Refused, with the field named, rather than silently degraded:
|
|
84
|
+
|
|
85
|
+
| | |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `$ref`, `$defs`, `oneOf`, `allOf`, `not`, `patternProperties` | `400 unsupported_schema` |
|
|
88
|
+
| a schema or prompt that would not fit the window | `400 context_length_exceeded` |
|
|
89
|
+
| `stream: true` | `400 stream_unsupported` |
|
|
90
|
+
| `tools:` | `400 tools_unsupported` |
|
|
91
|
+
| `n` other than 1 | `400 n_unsupported` |
|
|
92
|
+
| an API this macOS version does not have | `503 not_available` |
|
|
93
|
+
|
|
94
|
+
Dropping a constraint quietly would make `strict: true` a lie.
|
|
95
|
+
|
|
96
|
+
## Three things to know before you build on it
|
|
97
|
+
|
|
98
|
+
**The context window is 4096 tokens and the schema is charged to it.** With
|
|
99
|
+
the schema in the prompt each described field costs roughly 32 tokens —
|
|
100
|
+
about 40 fields before latency alone rules it out. The bridge preflights with
|
|
101
|
+
the model's own tokenizer and refuses over budget with a `400` naming the
|
|
102
|
+
field, rather than letting the generation fail with a `500`.
|
|
103
|
+
|
|
104
|
+
**It is serial by design.** The Swift helper reads one request, awaits the
|
|
105
|
+
generation, and only then reads the next. Apple Foundation Models is one
|
|
106
|
+
shared system daemon; concurrent callers see latency, never an error.
|
|
107
|
+
|
|
108
|
+
**Parse the answer; never string-match it.** Apple's structured output does
|
|
109
|
+
not emit object keys in a stable order — twenty identical generations come
|
|
110
|
+
back as twenty different byte strings and one value. Anything that hashes,
|
|
111
|
+
regexes or compares raw output will see differences that are not there.
|
|
112
|
+
|
|
113
|
+
## `/classify` — the bridge contract
|
|
114
|
+
|
|
115
|
+
`POST /classify` takes up to 100 `{id, text}` items and returns a
|
|
116
|
+
`{category, has_action, action}` for each, with **per-item** failures.
|
|
117
|
+
Unlike `/v1`, it runs the deterministic redaction pass over the model's free
|
|
118
|
+
text before returning it, because it is not a provider surface and nothing
|
|
119
|
+
downstream needs the bytes verbatim. A `/v1` caller should run its own —
|
|
120
|
+
Metistry's `completeJson()` scrubs every string leaf of the parsed result.
|
|
121
|
+
|
|
122
|
+
`GET /check` returns the frozen `check()` shape after running a **real**
|
|
123
|
+
classification end to end. It probes behaviour, never a permission API.
|
|
124
|
+
|
|
125
|
+
## Building the helper
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
pnpm --filter @foldedspacelabs/metistry-mcp-apple-fm build:helper
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Compiles `helper/afm-helper.swift` into a minimal app bundle and signs it —
|
|
132
|
+
Developer ID when one is installed (`METISTRY_SIGN_IDENTITY` pins which),
|
|
133
|
+
ad-hoc otherwise. The bundle shape buys this bridge nothing directly (no TCC
|
|
134
|
+
grants); it exists so both Swift helpers have one build shape to sign,
|
|
135
|
+
notarise and reason about.
|
|
136
|
+
|
|
137
|
+
## Where it came from
|
|
138
|
+
|
|
139
|
+
`docs/poc/poc19-apple-fm-v1/` is the proof that runtime-supplied schemas work
|
|
140
|
+
at all, and the reference implementation the translation here was lifted
|
|
141
|
+
from. `docs/ops/compute.md` is how a Metistry install wires it up as a
|
|
142
|
+
provider.
|
package/dist/helper.d.ts
CHANGED
|
@@ -3,14 +3,46 @@ export interface HelperResponse {
|
|
|
3
3
|
id: number;
|
|
4
4
|
ok: boolean;
|
|
5
5
|
error?: string;
|
|
6
|
+
/** the helper's own failure code — `unsupported_schema`, `context_length_exceeded`, `not_available`, … (v1.ts maps it to a status) */
|
|
7
|
+
code?: string;
|
|
6
8
|
classification?: {
|
|
7
9
|
category: string;
|
|
8
10
|
has_action: boolean;
|
|
9
11
|
action: string;
|
|
10
12
|
};
|
|
13
|
+
/** `op: "complete"` — the `/v1/chat/completions` engine room */
|
|
14
|
+
completion?: {
|
|
15
|
+
content: string;
|
|
16
|
+
prompt_tokens: number;
|
|
17
|
+
completion_tokens: number;
|
|
18
|
+
usage_ok: boolean;
|
|
19
|
+
schema_mode: string;
|
|
20
|
+
respond_ms: number;
|
|
21
|
+
};
|
|
22
|
+
/** `op: "models"` */
|
|
23
|
+
models?: Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
owned_by: string;
|
|
26
|
+
}>;
|
|
11
27
|
probe?: string;
|
|
12
28
|
category?: string;
|
|
13
29
|
ready?: boolean;
|
|
30
|
+
model?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* One line to the helper. `text` classifies; `op` names everything else.
|
|
34
|
+
* The helper answers strictly in order — it reads a line, awaits the
|
|
35
|
+
* generation, and only then reads the next — so several in-flight requests
|
|
36
|
+
* queue rather than run together (helper/afm-helper.swift: serial by design).
|
|
37
|
+
*/
|
|
38
|
+
export interface HelperRequest {
|
|
39
|
+
text?: string;
|
|
40
|
+
op?: string;
|
|
41
|
+
prompt?: string;
|
|
42
|
+
instructions?: string;
|
|
43
|
+
schema?: unknown;
|
|
44
|
+
temperature?: number;
|
|
45
|
+
max_tokens?: number;
|
|
14
46
|
}
|
|
15
47
|
export type Spawner = () => ChildProcess;
|
|
16
48
|
export declare class Helper {
|
|
@@ -21,10 +53,7 @@ export declare class Helper {
|
|
|
21
53
|
private nextId;
|
|
22
54
|
constructor(spawner: Spawner, timeoutMs?: number);
|
|
23
55
|
private ensure;
|
|
24
|
-
request(payload:
|
|
25
|
-
text?: string;
|
|
26
|
-
op?: string;
|
|
27
|
-
}): Promise<HelperResponse>;
|
|
56
|
+
request(payload: HelperRequest): Promise<HelperResponse>;
|
|
28
57
|
stop(): void;
|
|
29
58
|
}
|
|
30
59
|
export declare function defaultSpawner(binaryPath: string): Spawner;
|
package/dist/helper.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,qDAAqD;AAErD,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
2
|
import type { Helper } from "./helper.js";
|
|
3
|
+
import { MODEL_ID } from "./v1.js";
|
|
3
4
|
export interface BridgeConfig {
|
|
4
5
|
token: string;
|
|
5
6
|
}
|
|
6
7
|
export declare function makeBridge(helper: Helper, cfg: BridgeConfig): Server;
|
|
8
|
+
export { MODEL_ID };
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,26 @@
|
|
|
4
4
|
// over MODEL OUTPUT (§4.3 default 3 — PoC-13's OTP lesson: the helper's
|
|
5
5
|
// action field is model-generated text and gets scrubbed before anyone
|
|
6
6
|
// sees it).
|
|
7
|
+
//
|
|
8
|
+
// TWO SURFACES, ONE LISTENER, ONE BEARER (PR 4, PoC-19):
|
|
9
|
+
//
|
|
10
|
+
// /check, /classify the bridge contract — Metistry's error envelope
|
|
11
|
+
// /v1/models, an OpenAI-compatible provider, so `compute.yaml` can
|
|
12
|
+
// /v1/chat/completions list Apple Foundation Models with the same four
|
|
13
|
+
// lines as LM Studio and the engine needs no new code
|
|
14
|
+
//
|
|
15
|
+
// They share the listener and the auth on purpose. Invariant 8 says the
|
|
16
|
+
// network is not a boundary: a `/v1` that skipped the bearer because "it is
|
|
17
|
+
// only a local model" would be an unauthenticated completion endpoint on
|
|
18
|
+
// loopback, which is precisely the thing the rest of this file exists to
|
|
19
|
+
// refuse. The engine presents the bridge token like any other provider
|
|
20
|
+
// secret — `auth: { secret: METISTRY_BRIDGE_TOKEN_APPLE_FM }` in
|
|
21
|
+
// `compute.yaml`, resolved from the environment `metistry secrets sync --to
|
|
22
|
+
// env` writes.
|
|
23
|
+
import { randomUUID } from "node:crypto";
|
|
7
24
|
import { createServer } from "node:http";
|
|
8
25
|
import { authorized, errorEnvelope, statusFor, scrubModelOutput, runCheck, } from "@foldedspacelabs/metistry-core";
|
|
26
|
+
import { MODEL_ID, chatCompletion, modelsList, openAiError, statusForHelperCode, translateChatRequest, typeForHelperCode, } from "./v1.js";
|
|
9
27
|
function send(res, status, body) {
|
|
10
28
|
const text = JSON.stringify(body);
|
|
11
29
|
res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
@@ -22,11 +40,42 @@ async function readJson(req) {
|
|
|
22
40
|
}
|
|
23
41
|
export function makeBridge(helper, cfg) {
|
|
24
42
|
return createServer(async (req, res) => {
|
|
43
|
+
const path = new URL(req.url ?? "/", "http://x").pathname;
|
|
44
|
+
const v1 = path.startsWith("/v1/");
|
|
25
45
|
try {
|
|
26
|
-
// every caller authenticates — no unauthenticated surface at all
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
// every caller authenticates — no unauthenticated surface at all.
|
|
47
|
+
// `/v1` answers in OpenAI's error shape because its caller is an
|
|
48
|
+
// OpenAI client; the refusal itself is identical.
|
|
49
|
+
if (!authorized(req.headers.authorization, cfg.token)) {
|
|
50
|
+
return v1 ? send(res, 401, openAiError("authentication required", "unauthenticated", "invalid_request_error")) : fail(res, "unauthenticated");
|
|
51
|
+
}
|
|
52
|
+
const key = `${req.method} ${path}`;
|
|
53
|
+
if (key === "GET /v1/models")
|
|
54
|
+
return send(res, 200, modelsList());
|
|
55
|
+
if (key === "POST /v1/chat/completions") {
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await readJson(req);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return send(res, 400, openAiError("body is not JSON", "bad_json"));
|
|
62
|
+
}
|
|
63
|
+
const t = translateChatRequest(raw);
|
|
64
|
+
if (!t.ok)
|
|
65
|
+
return send(res, t.status, t.body);
|
|
66
|
+
const r = await helper.request(t.request);
|
|
67
|
+
if (!r.ok || !r.completion) {
|
|
68
|
+
const status = statusForHelperCode(r.code);
|
|
69
|
+
// The helper's message is its own (a schema it could not translate,
|
|
70
|
+
// a window it would have overrun) and is what makes the refusal
|
|
71
|
+
// actionable, so it crosses the wire verbatim. It never contains
|
|
72
|
+
// the caller's content — every one of them names a FIELD.
|
|
73
|
+
return send(res, status, openAiError(r.error ?? "the Apple Foundation Models helper did not answer", r.code ?? "internal", typeForHelperCode(r.code)));
|
|
74
|
+
}
|
|
75
|
+
return send(res, 200, chatCompletion(r.completion, { id: randomUUID() }));
|
|
76
|
+
}
|
|
77
|
+
if (v1)
|
|
78
|
+
return send(res, 404, openAiError(`no such endpoint: ${path} — this provider serves /v1/models and /v1/chat/completions`, "not_found"));
|
|
30
79
|
if (key === "GET /check") {
|
|
31
80
|
const result = await runCheck("apple-fm", "real classification of a canned item through FoundationModels", async () => {
|
|
32
81
|
const r = await helper.request({ op: "check" });
|
|
@@ -64,9 +113,14 @@ export function makeBridge(helper, cfg) {
|
|
|
64
113
|
return fail(res, "not_found");
|
|
65
114
|
}
|
|
66
115
|
catch {
|
|
67
|
-
if (!res.headersSent)
|
|
68
|
-
|
|
116
|
+
if (!res.headersSent) {
|
|
117
|
+
if (v1)
|
|
118
|
+
send(res, 500, openAiError("internal error", "internal", "server_error"));
|
|
119
|
+
else
|
|
120
|
+
fail(res, "internal");
|
|
121
|
+
}
|
|
69
122
|
}
|
|
70
123
|
});
|
|
71
124
|
}
|
|
125
|
+
export { MODEL_ID };
|
|
72
126
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,yEAAyE;AACzE,2EAA2E;AAC3E,wEAAwE;AACxE,uEAAuE;AACvE,YAAY;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/v1.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** The one model id this provider serves, matching the helper's `MODEL_ID`. */
|
|
2
|
+
export declare const MODEL_ID = "foundation-model";
|
|
3
|
+
export interface OpenAiError {
|
|
4
|
+
error: {
|
|
5
|
+
message: string;
|
|
6
|
+
type: string;
|
|
7
|
+
code: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function openAiError(message: string, code: string, type?: string): OpenAiError;
|
|
11
|
+
/** The helper's own code for a failure → the HTTP status the caller sees. Anything unrecognised is a 500: an unknown failure is ours, not the caller's. */
|
|
12
|
+
export declare function statusForHelperCode(code: string | undefined): number;
|
|
13
|
+
/** OpenAI's `error.type` for the same code, so a client's own error handling sees the class it expects. */
|
|
14
|
+
export declare function typeForHelperCode(code: string | undefined): string;
|
|
15
|
+
export declare function modelsList(now?: number): unknown;
|
|
16
|
+
/** What the helper is asked for: one prompt, optional instructions, optional schema. */
|
|
17
|
+
export interface HelperComplete {
|
|
18
|
+
op: "complete";
|
|
19
|
+
prompt: string;
|
|
20
|
+
instructions?: string;
|
|
21
|
+
schema?: unknown;
|
|
22
|
+
temperature?: number;
|
|
23
|
+
max_tokens?: number;
|
|
24
|
+
}
|
|
25
|
+
export type Translated = {
|
|
26
|
+
ok: true;
|
|
27
|
+
request: HelperComplete;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
status: number;
|
|
31
|
+
body: OpenAiError;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* An OpenAI chat request → the helper's request.
|
|
35
|
+
*
|
|
36
|
+
* `system`/`developer` messages become Foundation Models' `Instructions`;
|
|
37
|
+
* everything else becomes one prompt — unlabelled when there is a single
|
|
38
|
+
* turn, role-labelled when the caller sent a history. (PoC-19 wrote the
|
|
39
|
+
* multi-turn form but measured only the single-turn one; it is exercised
|
|
40
|
+
* here by a unit test and by nothing else yet.)
|
|
41
|
+
*/
|
|
42
|
+
export declare function translateChatRequest(raw: unknown): Translated;
|
|
43
|
+
export interface HelperCompletion {
|
|
44
|
+
content: string;
|
|
45
|
+
prompt_tokens: number;
|
|
46
|
+
completion_tokens: number;
|
|
47
|
+
/** false on macOS < 26.4, where `tokenCount(for:)` does not exist */
|
|
48
|
+
usage_ok: boolean;
|
|
49
|
+
schema_mode: string;
|
|
50
|
+
respond_ms: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The helper's completion → an OpenAI `chat.completion`.
|
|
54
|
+
*
|
|
55
|
+
* `usage` is OMITTED, not zeroed, when the helper could not count: a
|
|
56
|
+
* confident `{prompt_tokens: 0}` would be a lie the `runs` row would then
|
|
57
|
+
* repeat. `x_metistry` says which happened; every OpenAI client ignores it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function chatCompletion(c: HelperCompletion, opts: {
|
|
60
|
+
id: string;
|
|
61
|
+
now?: number;
|
|
62
|
+
}): unknown;
|
package/dist/v1.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// The OpenAI-compatible half of the bridge: `GET /v1/models` and
|
|
2
|
+
// `POST /v1/chat/completions`, translated to and from the Swift helper's
|
|
3
|
+
// stdio protocol. Pure functions — no sockets, no child process — so every
|
|
4
|
+
// refusal below is a unit test rather than a thing you find out in
|
|
5
|
+
// production (PoC-19's design, with the wire contract it did not have).
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS SHAPE AT ALL. The engine already speaks `/v1` to every provider
|
|
8
|
+
// (`apps/assistant/src/engine-openai.ts`). Making Apple Foundation Models a
|
|
9
|
+
// provider rather than a special case means `compute.yaml` can point at it
|
|
10
|
+
// with the same four lines as LM Studio, and nothing engine-side changes
|
|
11
|
+
// except the rule that on-machine providers run with reasoning off. The
|
|
12
|
+
// research note called this option (b); option (a) — a bespoke `kind:
|
|
13
|
+
// apple-fm` over `/classify` — stays rejected.
|
|
14
|
+
//
|
|
15
|
+
// WHAT IS DELIBERATELY REFUSED, each with the field named:
|
|
16
|
+
// stream: true no SSE mapping is proven (PoC-19 did not verify it)
|
|
17
|
+
// tools: tool calling is out of scope for v1
|
|
18
|
+
// n > 1 one generation per request; serial by design
|
|
19
|
+
// embeddings Apple exposes no embedding model here
|
|
20
|
+
//
|
|
21
|
+
// ERROR SHAPE. `/check` and `/classify` keep Metistry's uniform envelope
|
|
22
|
+
// (`{error:{code,message}}`, core's `errorEnvelope`). `/v1/*` answers in
|
|
23
|
+
// OpenAI's (`{error:{message,type,code}}`) because its caller is an OpenAI
|
|
24
|
+
// client that reads `error.message` — the same reason the route exists.
|
|
25
|
+
// Both carry a `code`; only the sibling fields differ.
|
|
26
|
+
//
|
|
27
|
+
// REDACTION moves, and does not vanish. `/classify` scrubs the model's
|
|
28
|
+
// `action` text before returning it (PoC-13's OTP lesson). `/v1` cannot: a
|
|
29
|
+
// provider surface must hand back exactly what the model said, or a JSON
|
|
30
|
+
// payload comes back corrupted and `strict: true` was a lie. So the scrub
|
|
31
|
+
// moves to the caller — `completeJson()` in the collectors package runs it
|
|
32
|
+
// over every string leaf of the parsed result, which is where `/classify`'s
|
|
33
|
+
// guarantee lived anyway.
|
|
34
|
+
/** The one model id this provider serves, matching the helper's `MODEL_ID`. */
|
|
35
|
+
export const MODEL_ID = "foundation-model";
|
|
36
|
+
export function openAiError(message, code, type = "invalid_request_error") {
|
|
37
|
+
return { error: { message, type, code } };
|
|
38
|
+
}
|
|
39
|
+
/** The helper's own code for a failure → the HTTP status the caller sees. Anything unrecognised is a 500: an unknown failure is ours, not the caller's. */
|
|
40
|
+
export function statusForHelperCode(code) {
|
|
41
|
+
switch (code) {
|
|
42
|
+
case "invalid_request":
|
|
43
|
+
case "unsupported_schema":
|
|
44
|
+
case "context_length_exceeded":
|
|
45
|
+
return 400;
|
|
46
|
+
case "not_available":
|
|
47
|
+
case "model_unavailable":
|
|
48
|
+
return 503;
|
|
49
|
+
default:
|
|
50
|
+
return 500;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** OpenAI's `error.type` for the same code, so a client's own error handling sees the class it expects. */
|
|
54
|
+
export function typeForHelperCode(code) {
|
|
55
|
+
return statusForHelperCode(code) === 400 ? "invalid_request_error" : "server_error";
|
|
56
|
+
}
|
|
57
|
+
export function modelsList(now = Date.now()) {
|
|
58
|
+
return {
|
|
59
|
+
object: "list",
|
|
60
|
+
data: [{ id: MODEL_ID, object: "model", created: Math.floor(now / 1000), owned_by: "apple" }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const bad = (message, code, status = 400) => ({ ok: false, status, body: openAiError(message, code) });
|
|
64
|
+
/**
|
|
65
|
+
* An OpenAI chat request → the helper's request.
|
|
66
|
+
*
|
|
67
|
+
* `system`/`developer` messages become Foundation Models' `Instructions`;
|
|
68
|
+
* everything else becomes one prompt — unlabelled when there is a single
|
|
69
|
+
* turn, role-labelled when the caller sent a history. (PoC-19 wrote the
|
|
70
|
+
* multi-turn form but measured only the single-turn one; it is exercised
|
|
71
|
+
* here by a unit test and by nothing else yet.)
|
|
72
|
+
*/
|
|
73
|
+
export function translateChatRequest(raw) {
|
|
74
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
75
|
+
return bad("body is not a JSON object", "bad_json");
|
|
76
|
+
const body = raw;
|
|
77
|
+
if (body.stream === true)
|
|
78
|
+
return bad("`stream` is not implemented — this provider answers in one piece (docs/ops/compute.md \"Apple Foundation Models\")", "stream_unsupported");
|
|
79
|
+
if (body.tools !== undefined)
|
|
80
|
+
return bad("`tools` is not supported: tool calling is out of scope for this provider's v1 surface", "tools_unsupported");
|
|
81
|
+
if (typeof body.n === "number" && body.n !== 1)
|
|
82
|
+
return bad("`n` must be 1 — this provider generates one completion per request, serially", "n_unsupported");
|
|
83
|
+
const messages = body.messages;
|
|
84
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
85
|
+
return bad("`messages` is required and must be a non-empty array", "missing_messages");
|
|
86
|
+
const systemParts = [];
|
|
87
|
+
const turns = [];
|
|
88
|
+
for (const m of messages) {
|
|
89
|
+
if (typeof m !== "object" || m === null)
|
|
90
|
+
return bad("every entry of `messages` must be an object with a role and string content", "invalid_message");
|
|
91
|
+
const msg = m;
|
|
92
|
+
const role = typeof msg.role === "string" ? msg.role : "user";
|
|
93
|
+
// Only string content: this provider has no vision and no audio, so a
|
|
94
|
+
// content-parts array would be silently dropping half the request.
|
|
95
|
+
if (typeof msg.content !== "string")
|
|
96
|
+
return bad("`messages[].content` must be a string — this provider takes no content parts, images or audio", "invalid_message");
|
|
97
|
+
if (role === "system" || role === "developer")
|
|
98
|
+
systemParts.push(msg.content);
|
|
99
|
+
else
|
|
100
|
+
turns.push({ role, content: msg.content });
|
|
101
|
+
}
|
|
102
|
+
if (turns.length === 0)
|
|
103
|
+
return bad("`messages` carries only system content — add a user message", "missing_messages");
|
|
104
|
+
const request = {
|
|
105
|
+
op: "complete",
|
|
106
|
+
prompt: turns.length === 1 ? turns[0].content : turns.map((t) => `${t.role}: ${t.content}`).join("\n"),
|
|
107
|
+
...(systemParts.length > 0 ? { instructions: systemParts.join("\n\n") } : {}),
|
|
108
|
+
};
|
|
109
|
+
const rf = body.response_format;
|
|
110
|
+
if (rf !== undefined) {
|
|
111
|
+
if (typeof rf !== "object" || rf === null)
|
|
112
|
+
return bad("`response_format` must be an object", "unsupported_response_format");
|
|
113
|
+
const kind = rf.type;
|
|
114
|
+
if (kind === "json_schema") {
|
|
115
|
+
const js = rf.json_schema;
|
|
116
|
+
const schema = typeof js === "object" && js !== null ? js.schema : undefined;
|
|
117
|
+
if (schema === undefined)
|
|
118
|
+
return bad("`response_format.json_schema.schema` is required", "missing_schema");
|
|
119
|
+
request.schema = schema;
|
|
120
|
+
}
|
|
121
|
+
else if (kind === "json_object") {
|
|
122
|
+
return bad("`response_format: json_object` has no schema to constrain on — use `json_schema`, which this provider enforces at generation time", "unsupported_response_format");
|
|
123
|
+
}
|
|
124
|
+
else if (kind !== "text") {
|
|
125
|
+
return bad(`unsupported \`response_format.type\`: ${JSON.stringify(kind)}`, "unsupported_response_format");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (body.temperature !== undefined) {
|
|
129
|
+
if (typeof body.temperature !== "number" || body.temperature < 0)
|
|
130
|
+
return bad("`temperature` must be a non-negative number", "invalid_request");
|
|
131
|
+
if (body.temperature > 0)
|
|
132
|
+
request.temperature = body.temperature;
|
|
133
|
+
}
|
|
134
|
+
const max = body.max_tokens ?? body.max_completion_tokens;
|
|
135
|
+
if (max !== undefined) {
|
|
136
|
+
if (typeof max !== "number" || !Number.isInteger(max) || max < 1)
|
|
137
|
+
return bad("`max_tokens` must be a positive integer", "invalid_request");
|
|
138
|
+
request.max_tokens = max;
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, request };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The helper's completion → an OpenAI `chat.completion`.
|
|
144
|
+
*
|
|
145
|
+
* `usage` is OMITTED, not zeroed, when the helper could not count: a
|
|
146
|
+
* confident `{prompt_tokens: 0}` would be a lie the `runs` row would then
|
|
147
|
+
* repeat. `x_metistry` says which happened; every OpenAI client ignores it.
|
|
148
|
+
*/
|
|
149
|
+
export function chatCompletion(c, opts) {
|
|
150
|
+
return {
|
|
151
|
+
id: `chatcmpl-${opts.id}`,
|
|
152
|
+
object: "chat.completion",
|
|
153
|
+
created: Math.floor((opts.now ?? Date.now()) / 1000),
|
|
154
|
+
model: MODEL_ID,
|
|
155
|
+
choices: [{ index: 0, message: { role: "assistant", content: c.content }, finish_reason: "stop" }],
|
|
156
|
+
...(c.usage_ok
|
|
157
|
+
? { usage: { prompt_tokens: c.prompt_tokens, completion_tokens: c.completion_tokens, total_tokens: c.prompt_tokens + c.completion_tokens } }
|
|
158
|
+
: {}),
|
|
159
|
+
x_metistry: {
|
|
160
|
+
cost_usd: 0,
|
|
161
|
+
schema_mode: c.schema_mode,
|
|
162
|
+
respond_ms: c.respond_ms,
|
|
163
|
+
...(c.usage_ok ? {} : { usage: "unavailable: tokenCount(for:) needs macOS 26.4" }),
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=v1.js.map
|
package/dist/v1.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v1.js","sourceRoot":"","sources":["../src/v1.ts"],"names":[],"mappings":"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"}
|
package/manifest.yaml
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
name: apple-fm
|
|
2
2
|
type: bridge
|
|
3
|
-
description:
|
|
3
|
+
description: Apple Foundation Models on-device — an OpenAI-compatible /v1 provider surface (runtime JSON schemas, PoC-19) plus the classify route (PoC-3). Free tier, cost 0.
|
|
4
4
|
transport: http
|
|
5
5
|
port: 7810
|
|
6
6
|
runs_on: host # FoundationModels is macOS-only; NO TCC grants needed
|
|
7
7
|
requires_tcc: []
|
|
8
8
|
discovery: eager # 2 tools — far under the >20/>5k lazy threshold (PoC-17)
|
|
9
9
|
degrades: absent # router stops offering classification when absent
|
|
10
|
+
# `exposes:` is the AGENT-facing surface — what a tool caller may invoke.
|
|
11
|
+
# `/v1/models` and `/v1/chat/completions` are deliberately NOT listed: they
|
|
12
|
+
# are a provider wire for the engine and for collectors, not a tool the
|
|
13
|
+
# assistant may reach for. Invariant 9 by absence — nothing in this list can
|
|
14
|
+
# make the assistant run a model of its choosing (docs/ops/compute.md).
|
|
10
15
|
exposes:
|
|
11
16
|
- name: classify
|
|
12
17
|
description: Classify inbox capture items (category, has_action, action)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldedspacelabs/metistry-mcp-apple-fm",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Metistry Apple Foundation Models bridge: free on-device classification over the §4.3 wire contract (HTTP + bearer auth + check()). Ships a compiled Swift helper; no TCC grants required. Usable standalone.",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Metistry Apple Foundation Models bridge: an OpenAI-compatible /v1 surface (runtime JSON schemas) plus free on-device classification, over the §4.3 wire contract (HTTP + bearer auth + check()). Ships a compiled Swift helper; no TCC grants required. Usable standalone.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
14
|
"helper/afm-helper.app",
|
|
15
|
-
"manifest.yaml"
|
|
15
|
+
"manifest.yaml",
|
|
16
|
+
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"engines": {
|
|
18
19
|
"node": ">=22"
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
"darwin"
|
|
22
23
|
],
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@foldedspacelabs/metistry-core": "0.
|
|
25
|
+
"@foldedspacelabs/metistry-core": "0.8.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@types/node": "^22.20.1",
|