diff --git a/.gitignore b/.gitignore index 0c2047d..b33c4e7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ dist build-native.log foundation-models-c .build -native/ +native/* +!native/extensions/ coverage .vscode/settings.json docs/.vitepress/dist diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f8c55c0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +dist +coverage +native +docs/node_modules +docs/.vitepress/dist +docs/.vitepress/cache +package-lock.json +*.md diff --git a/.prettierrc b/.prettierrc index e9bd086..4a1222f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,4 +4,4 @@ "trailingComma": "all", "printWidth": 100, "tabWidth": 2 -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd04af..a77d3e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `generable()` — declarative typed schema builder for structured output with full TypeScript type inference, the equivalent of the Python SDK's `@generable` decorator +- `SystemLanguageModel.contextSize` — read the model's context window size (back-deployed from macOS 26.4 SDK) +- `SystemLanguageModel.supportedLanguages` — list supported language codes +- `SystemLanguageModel.supportsLocale()` — check if a specific locale is supported +- `LanguageModelSession.prewarm()` — preload model resources and optionally cache a prompt prefix to reduce first-response latency +- `GeneratedContent.dispose()` / `Symbol.dispose` — explicit resource cleanup for structured output results, with `FinalizationRegistry` auto-cleanup as a safety net +- `NativeTypeName` type export — compound array type names (`"array"`, `"array"`, etc.) for use with `GenerationSchema.property()` +- `decodeString()` — decode a C string pointer without freeing it, for use in callbacks where the C side owns the memory +- `Tool.onCall` now receives parsed arguments as a second parameter: `(toolName, args)` instead of `(toolName)` +- Input validation: `temperature` must be ≥ 0, `maximumResponseTokens` must be a positive integer — both throw immediately on invalid values +- Explicit FFI type casts (`as NativePointer`, `as boolean`, etc.) at all C call sites +- 3 new examples: `contact-card` (nested generable schemas), `email-triage` (JSON Schema + streaming + tools), `journal` (tools + transcript persistence) +- ESLint: `no-floating-promises` and `no-console` for `src/`, `no-eval` and `no-debugger` globally +- Unit tests for `generable()`, streaming edge cases, compat `reorderJson` with array items, disposed session guards, stream queue-stall recovery, and all 3 new examples + +### Fixed + +- Stream setup failures no longer stall the request queue permanently (native init moved inside try/finally) +- Stream idle timeout (30s) prevents permanent hangs when native callbacks stop firing after tool-call snapshots +- Disposed session methods (`respond`, `respondWithSchema`, `respondWithJsonSchema`, `streamResponse`) now throw `FoundationModelsError` immediately instead of calling into freed native memory +- `FinalizationRegistry` callbacks across all classes now log warnings via `console.warn` instead of silently swallowing errors +- Better error message when `libFoundationModels.dylib` is not found — lists all searched paths and suggests `npm run build` +- Streaming callback now receives content as `void*` instead of `str` to prevent koffi from coercing null C string pointers to the JS string `"null"` +- Streaming iterator now resets the session (`FMLanguageModelSessionReset`) on early `break` to prevent stalled subsequent calls +- Coerced `"null"` string chunks from koffi are filtered out during streaming + +### Changed + +- `generable()` array properties now use compound type names (`"array"`, `"array"`) matching the Python SDK's C bridge convention +- `GenerationSchema.property()` rejects bare `"array"` type — use compound form like `"array"` or use `generable()` for automatic type resolution +- Prettier scope widened from `src/` to entire repo (excluding `*.md`); added `.prettierignore` +- Standardized "Apple Foundation Models" terminology (dropped possessive "'s") across docs and config +- README license section rewritten with copyright notice and Apple trademark disclaimer +- `docs/tsconfig.json` added for VitePress theme type checking +- CSS: added `.VPHero .tagline` max-width constraints for responsive layout + ## [0.3.1] - 2026-03-12 ### Added @@ -121,7 +159,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- TypeScript/Node.js bindings for Apple's Foundation Models framework via koffi FFI +- TypeScript/Node.js bindings for Apple Foundation Models framework via koffi FFI - `SystemLanguageModel` class with availability checks and `waitUntilAvailable()` - `LanguageModelSession` with `respond()`, `streamResponse()`, and `respondWithJsonSchema()` for text, streaming, and structured generation - `GenerationSchema` and `GenerationSchemaProperty` for typed structured output with generation guides diff --git a/README.md b/README.md index 47368f2..d4c9ee6 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,11 @@ model.dispose(); ## Requirements -- Mac running macOS 26 (Tahoe) or later on Apple Silicon +- Apple Silicon (M-Series) Mac running macOS 26 or later - Apple Intelligence enabled in System Settings - Node.js 20+ -Xcode **not** required (prebuilt dylib is bundled with the npm package) +Unless you are building from source, Xcode is not required. ## Development @@ -84,5 +84,10 @@ Issues and PRs welcome. If something doesn't work on your machine or you find a ## License -Apache 2.0 - See [LICENSE.md](LICENSE.md) -The npm package bundles Apple's Foundation Models C bindings and prebuilt dylib (also Apache 2.0 - see [NOTICE](NOTICE)) +© 2026 Cody Bromley and contributors. + +tsfm is licensed under the Apache 2.0 license. For complete licensing information, see this project's [LICENSE file](LICENSE.md). + +The `tsfm-sdk` package available from NPM contains precompiled C bindings and libraries for working with macOS 26 Foundation Models adapted from [python-apple-fm-sdk](https://github.com/apple/python-apple-fm-sdk) which is Copyright Apple Inc. and licensed under the Apache 2.0 license. + +This project is unaffiliated with Apple, Inc. The terms "Apple" and "Apple Intelligence" are trademarks of Apple Inc., registered in the U.S. and other countries and regions. diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 0e5a48f..1cdbd93 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -79,7 +79,7 @@ function guideSidebar() { export default defineConfig({ title: "tsfm", description: - "TypeScript SDK for Apple's Foundation Models framework — on-device Apple Intelligence in Node.js", + "TypeScript SDK for Apple Foundation Models framework — on-device Apple Intelligence in Node.js", base: "/", cleanUrls: true, @@ -109,7 +109,7 @@ export default defineConfig({ { property: "og:description", content: - "TypeScript SDK for Apple's Foundation Models — on-device AI inference in Node.js. No keys. No fees. It just works.", + "TypeScript SDK for Apple Foundation Models — on-device AI inference in Node.js. No keys. No fees. It just works.", }, ], ["meta", { property: "og:image", content: "https://tsfm.dev/og-image.png" }], @@ -125,7 +125,7 @@ export default defineConfig({ { name: "twitter:description", content: - "TypeScript SDK for Apple's Foundation Models. On-device AI inference in Node.js. No keys. No fees. It just works.", + "TypeScript SDK for Apple Foundation Models. On-device AI inference in Node.js. No keys. No fees. It just works.", }, ], ["meta", { name: "twitter:image", content: "https://tsfm.dev/og-image.png" }], diff --git a/docs/.vitepress/generate-llms.ts b/docs/.vitepress/generate-llms.ts index 8404ae6..d14078d 100644 --- a/docs/.vitepress/generate-llms.ts +++ b/docs/.vitepress/generate-llms.ts @@ -165,7 +165,7 @@ export function generateLlmTxt(): string { const lines: string[] = [ "# tsfm", "", - "> TypeScript SDK for Apple's Foundation Models framework — on-device Apple Intelligence inference in Node.js via FFI. macOS 26+, Apple Silicon only.", + "> TypeScript SDK for Apple Foundation Models framework — on-device Apple Intelligence inference in Node.js via FFI. macOS 26+, Apple Silicon only.", "", ]; @@ -218,7 +218,7 @@ export async function generateLlmsFullTxt(srcDir: string): Promise { const parts: string[] = [ "# tsfm — Complete Documentation", "", - "> TypeScript SDK for Apple's Foundation Models framework — on-device Apple Intelligence inference in Node.js via FFI. macOS 26+, Apple Silicon only.", + "> TypeScript SDK for Apple Foundation Models framework — on-device Apple Intelligence inference in Node.js via FFI. macOS 26+, Apple Silicon only.", "", ]; diff --git a/docs/.vitepress/theme/CopyPageButton.vue b/docs/.vitepress/theme/CopyPageButton.vue index 6b26613..c9305d7 100644 --- a/docs/.vitepress/theme/CopyPageButton.vue +++ b/docs/.vitepress/theme/CopyPageButton.vue @@ -6,7 +6,12 @@ {{ copied ? "Copied!" : "Copy page" }} -
@@ -17,31 +22,63 @@
Copy page as Markdown for LLMs
- +
-
View as Markdown
+
+ View as Markdown +
View this page as plain text
- +
-
Open in Claude
+
+ Open in Claude +
Ask questions about this page
- +
-
Open in ChatGPT
+
+ Open in ChatGPT +
Ask questions about this page
- +
-
View on GitHub
+
+ View on GitHub +
View source on GitHub
@@ -72,9 +109,7 @@ const rawUrl = computed( ); const pageUrl = computed(() => { - const path = page.value.relativePath - .replace(/\.md$/, "") - .replace(/\/index$/, "/"); + const path = page.value.relativePath.replace(/\.md$/, "").replace(/\/index$/, "/"); return `https://tsfm.dev/${path}`; }); @@ -89,8 +124,7 @@ const chatgptUrl = computed(() => { }); const githubUrl = computed( - () => - `https://github.com/codybrom/tsfm/blob/main/docs/${page.value.relativePath}`, + () => `https://github.com/codybrom/tsfm/blob/main/docs/${page.value.relativePath}`, ); async function copyPage() { diff --git a/docs/.vitepress/theme/HeroScene.vue b/docs/.vitepress/theme/HeroScene.vue index 6436141..36d318e 100644 --- a/docs/.vitepress/theme/HeroScene.vue +++ b/docs/.vitepress/theme/HeroScene.vue @@ -16,13 +16,12 @@ onMounted(async () => { const glowEl = glowRef.value; if (!el || !glowEl) return; - const [THREE, { LineSegments2 }, { LineSegmentsGeometry }, { LineMaterial }] = - await Promise.all([ - import("three"), - import("three/addons/lines/LineSegments2.js"), - import("three/addons/lines/LineSegmentsGeometry.js"), - import("three/addons/lines/LineMaterial.js"), - ]); + const [THREE, { LineSegments2 }, { LineSegmentsGeometry }, { LineMaterial }] = await Promise.all([ + import("three"), + import("three/addons/lines/LineSegments2.js"), + import("three/addons/lines/LineSegmentsGeometry.js"), + import("three/addons/lines/LineMaterial.js"), + ]); // --- Scene setup --- const scene = new THREE.Scene(); @@ -38,34 +37,51 @@ onMounted(async () => { el.appendChild(renderer.domElement); // --- Lattice shape --- + // 11 nodes: two poles (top/bot), a center, and two rings of 4 const S = 0.7; - const top = [0, 1, 0], bot = [0, -1, 0], mid = [0, 0, 0]; - const ulf = [-S, .4, S], ulb = [-S, .4, -S]; // upper-left front/back - const urf = [S, .4, S], urb = [S, .4, -S]; // upper-right front/back - const llf = [-S, -.4, S], llb = [-S, -.4, -S]; // lower-left front/back - const lrf = [S, -.4, S], lrb = [S, -.4, -S]; // lower-right front/back - - const allNodes = [top, ulf, ulb, urf, urb, mid, llf, llb, lrf, lrb, bot]; - - // Each pair of entries = one line segment - type P = number[]; - const seg = (...pairs: [P, P][]) => pairs.flatMap(([a, b]) => [...a, ...b]); - const linePoints = seg( - // top/bottom spokes - [top, ulf], [top, ulb], [top, urf], [top, urb], [top, mid], - [bot, llf], [bot, llb], [bot, lrf], [bot, lrb], [bot, mid], - // center spokes - [mid, ulf], [mid, ulb], [mid, urf], [mid, urb], - [mid, llf], [mid, llb], [mid, lrf], [mid, lrb], - // front face - [ulf, urf], [urf, lrf], [lrf, llf], [llf, ulf], [ulf, lrf], [urf, llf], - // back face - [ulb, urb], [urb, lrb], [lrb, llb], [llb, ulb], [ulb, lrb], [urb, llb], - // left face - [ulf, ulb], [ulb, llb], [llb, llf], [llf, ulf], [ulf, llb], [ulb, llf], - // right face - [urf, urb], [urb, lrb], [lrb, lrf], [lrf, urf], [urf, lrb], [urb, lrf], - ); + const top = [0, 1, 0], + bot = [0, -1, 0], + mid = [0, 0, 0]; + const ulf = [-S, 0.4, S], + ulb = [-S, 0.4, -S]; + const urf = [S, 0.4, S], + urb = [S, 0.4, -S]; + const llf = [-S, -0.4, S], + llb = [-S, -0.4, -S]; + const lrf = [S, -0.4, S], + lrb = [S, -0.4, -S]; + + const upper = [ulf, ulb, urf, urb]; + const lower = [llf, llb, lrf, lrb]; + const allNodes = [top, ...upper, mid, ...lower, bot]; + + // hub: connect one node to many others + const hub = (c: number[], ...spokes: number[][]) => spokes.flatMap((s) => [...c, ...s]); + // quad: 4 outline edges + 2 diagonal crossbars + const quad = (a: number[], b: number[], c: number[], d: number[]) => [ + ...a, + ...b, + ...b, + ...c, + ...c, + ...d, + ...d, + ...a, + ...a, + ...c, + ...b, + ...d, + ]; + + const linePoints = [ + ...hub(top, ...upper, mid), + ...hub(bot, ...lower, mid), + ...hub(mid, ...upper, ...lower), + ...quad(ulf, urf, lrf, llf), // front + ...quad(ulb, urb, lrb, llb), // back + ...quad(ulf, ulb, llb, llf), // left + ...quad(urf, urb, lrb, lrf), // right + ]; const group = new THREE.Group(); scene.add(group); diff --git a/docs/.vitepress/theme/HomeExplore.vue b/docs/.vitepress/theme/HomeExplore.vue index 0a7c47e..e3c664c 100644 --- a/docs/.vitepress/theme/HomeExplore.vue +++ b/docs/.vitepress/theme/HomeExplore.vue @@ -1,11 +1,6 @@