@foldedspacelabs/metistry-mcp-apple-fm 0.7.1 → 0.8.1

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 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;
@@ -1 +1 @@
1
- {"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"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"}
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
- if (!authorized(req.headers.authorization, cfg.token))
28
- return fail(res, "unauthenticated");
29
- const key = `${req.method} ${new URL(req.url ?? "/", "http://x").pathname}`;
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
- fail(res, "internal");
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":"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"}
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: On-device Foundation Models classification (free tier, PoC-3)
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.7.1",
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.1",
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.7.1"
25
+ "@foldedspacelabs/metistry-core": "0.8.1"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@types/node": "^22.20.1",