### Guides/Applescript # How to use the Apple Foundation Model from AppleScript Call Apple's on-device Foundation Model from AppleScript via `do shell script` + `curl`. 100% on-device - perfect for Shortcuts, Automator, and macOS system automation. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/applescript](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/applescript). ## Prerequisites - macOS 26+ Tahoe, Apple Silicon, Apple Intelligence enabled - `brew install apfel jq` - `apfel --serve` running (port `11434`) - AppleScript (ships with macOS) AppleScript has no native HTTP client; the idiomatic pattern is `do shell script "curl ..."`. ## 1. One-shot ```applescript set payload to "{\"model\":\"apple-foundationmodel\",\"messages\":[{\"role\":\"user\",\"content\":\"In one sentence, what is the Swift programming language?\"}],\"max_tokens\":80}" set response to do shell script "curl -sS http://localhost:11434/v1/chat/completions -H 'Content-Type: application/json' -d " & quoted form of payload & " | jq -r '.choices[0].message.content'" return response ``` Real output: ```text Swift is a modern, open-source programming language developed by Apple for developing iOS, macOS, watchOS, and tvOS applications. ``` Lab script: [`01_oneshot.applescript`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/applescript/01_oneshot.applescript). ## 2. Streaming AppleScript doesn't stream natively - `do shell script` returns the final string only. Streaming happens inside the shell pipeline: ```applescript set shellCmd to "curl -sS -N http://localhost:11434/v1/chat/completions " & ¬ "-H 'Content-Type: application/json' " & ¬ "-d '{\"model\":\"apple-foundationmodel\",\"messages\":[{\"role\":\"user\",\"content\":\"List three Apple silicon chips, one per line.\"}],\"max_tokens\":80,\"stream\":true}' " & ¬ "| while IFS= read -r line; do " & ¬ " line=\"${line#data: }\"; " & ¬ " [ -z \"$line\" ] || [ \"$line\" = \"[DONE]\" ] && continue; " & ¬ " content=$(printf '%s' \"$line\" | jq -r '.choices[0].delta.content // empty' 2>/dev/null || true); " & ¬ " [ -n \"$content\" ] && printf '%s' \"$content\"; " & ¬ " done; echo" return do shell script shellCmd ``` Real output: ```text Apple M1 Apple M1 Pro Apple M1 Max ``` Lab script: [`02_stream.applescript`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/applescript/02_stream.applescript). ## 3. JSON mode ```applescript set payload to "{\"model\":\"apple-foundationmodel\",\"messages\":[{\"role\":\"user\",\"content\":\"Return JSON with fields chip, year, cores. Describe the Apple M1 chip. Return ONLY JSON.\"}],\"response_format\":{\"type\":\"json_object\"},\"max_tokens\":120}" set cmd to "curl -sS http://localhost:11434/v1/chat/completions -H 'Content-Type: application/json' -d " & quoted form of payload & " | jq -r '.choices[0].message.content' | sed -E 's/^```(json)?//; s/```$//' | tr -d '\\r' | jq '.'" return do shell script cmd ``` Real output (note AppleScript collapses newlines when returning from `do shell script`): ```json { "chip": "Apple M1", "year": 2020, "cores": 8} ``` Lab script: [`03_json.applescript`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/applescript/03_json.applescript). ## 4. Error handling ```applescript set cmd to "tmp=$(mktemp); status=$(curl -sS -o \"$tmp\" -w '%{http_code}' http://localhost:11434/v1/embeddings -H 'Content-Type: application/json' -d '{\"model\":\"apple-foundationmodel\",\"input\":\"apfel runs 100% on-device.\"}'); if [ \"$status\" -ge 400 ]; then msg=$(jq -r '.error.message // empty' \"$tmp\" 2>/dev/null || true); echo \"Got expected error: HTTP $status - ${msg:-see response}\"; else echo \"unexpected success: HTTP $status\"; cat \"$tmp\"; fi; rm -f \"$tmp\"" return do shell script cmd ``` Real output: ```text Got expected error: HTTP 501 - Embeddings not supported by Apple's on-device model. ``` Lab script: [`04_errors.applescript`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/applescript/04_errors.applescript). ## 5. Tool calling (delegate to Bash) Tool calling from pure AppleScript is not idiomatic - the required JSON escaping becomes unreadable fast. The correct AppleScript pattern is to delegate complex shell work to a script file. Reuse the Bash tool-calling script: ```applescript set scriptPath to POSIX path of ((path to me as text) & "::") & "../bash-curl/05_tools.sh" return do shell script "bash " & quoted form of scriptPath ``` Real output: ```text The current temperature in Vienna is 14 degrees Celsius. ``` Lab script: [`05_tools.applescript`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/applescript/05_tools.applescript). For production tool-calling, use [python.md](python.md) or [nodejs.md](nodejs.md). ## 6. Real example - summarize a file AppleScript cannot read stdin inside `do shell script`, so pass a file path on argv: ```applescript on run argv if (count of argv) < 1 then error "usage: osascript 06_example.applescript " set filePath to item 1 of argv set cmd to "text=$(cat " & quoted form of filePath & "); " & ¬ "payload=$(jq -n --arg text \"$text\" '{model:\"apple-foundationmodel\", messages:[{role:\"system\",content:\"You are a concise summarizer. Reply with one short paragraph.\"},{role:\"user\",content:(\"Summarize:\\n\\n\" + $text)}], max_tokens:150}'); " & ¬ "curl -sS http://localhost:11434/v1/chat/completions -H 'Content-Type: application/json' -d \"$payload\" | jq -r '.choices[0].message.content'" return do shell script cmd end run ``` Usage: `osascript 06_example.applescript /path/to/file.txt` Real output: ```text In November 2020, Apple released the M1 chip, the first ARM-based system-on-a-chip for Mac computers. The chip features an 8-core CPU with four performance and four efficiency cores, an integrated GPU with up to 8 cores, and a unified CPU, GPU, memory, and neural engine on a single die. The M1 chip offers significant performance-per-watt improvements over the Intel chips it replaced. ``` Lab script: [`06_example.applescript`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/applescript/06_example.applescript). ## Shortcuts integration Paste any of these into a **Run AppleScript** action in Shortcuts. Combine with **Get Contents of Clipboard** to summarize whatever you just copied - all on-device, no network call. ## Troubleshooting - **Collapsed newlines** - `do shell script` returns a single AppleScript string with all newlines folded. That's a Classic macOS quirk, not an apfel issue. - **Stdin not flowing** - AppleScript cannot pipe its own stdin into `do shell script`. Pass file paths via `on run argv` instead. - **Escaping** - always use `quoted form of` for any user-supplied string before embedding in a shell command. ## Tested with - apfel v1.0.3 / macOS 26.3.1 Apple Silicon (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - osascript / AppleScript (system) / jq 1.7 - Date: 2026-04-16 Runnable tests: [tests/test_applescript.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_applescript.py). ## See also [bash-curl.md](bash-curl.md), [zsh.md](zsh.md), [swift-scripting.md](swift-scripting.md), [apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) --- ### Guides/Awk # How to use the Apple Foundation Model from AWK AWK can't do HTTP on its own - it was designed for text processing, not networking. The UNIX convention is to **pair AWK with curl**: curl handles transport, AWK parses the response. That's what every script in this guide does. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/awk](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/awk). ## Prerequisites - macOS 26+ Tahoe, Apple Silicon, Apple Intelligence enabled - `brew install apfel jq` (`jq` is only needed for the JSON-mode + tool-calling examples) - `apfel --serve` running (port `11434`) - `awk` (ships with macOS) ## 1. One-shot ```bash #!/usr/bin/env bash set -euo pipefail PROMPT="In one sentence, what is the Swift programming language?" PAYLOAD=$(awk -v prompt="$PROMPT" 'BEGIN { gsub(/"/, "\\\"", prompt) printf "{\"model\":\"apple-foundationmodel\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}],\"max_tokens\":80}", prompt }') curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" -d "$PAYLOAD" \ | awk 'BEGIN { RS="\"content\" :" } NR==2 { match($0, /"([^"\\]|\\.)*"/) s = substr($0, RSTART+1, RLENGTH-2) gsub(/\\n/, "\n", s); gsub(/\\"/, "\"", s); gsub(/\\\\/, "\\", s) print s }' ``` Real output: ```text Swift is a modern, open-source programming language developed by Apple for developing apps and systems across platforms, known for its safety, performance, and ease of use. ``` Lab script: [`01_oneshot.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/awk/01_oneshot.sh). ## 2. Streaming ```bash #!/usr/bin/env bash set -euo pipefail curl -sS -N http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"apple-foundationmodel","messages":[{"role":"user","content":"List three Apple silicon chips, one per line."}],"max_tokens":80,"stream":true}' \ | awk ' /^data: / { json = substr($0, 7) if (json == "[DONE]" || json == "") next if (match(json, /"content":"([^"\\]|\\.)*"/)) { s = substr(json, RSTART + 11, RLENGTH - 12) gsub(/\\n/, "\n", s); gsub(/\\"/, "\"", s); gsub(/\\\\/, "\\", s) printf "%s", s fflush() } } END { print "" } ' ``` Real output: ```text Apple M1 Apple M1 Pro Apple M1 Max ``` Lab script: [`02_stream.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/awk/02_stream.sh). ## 3. JSON mode AWK is not a JSON parser. It can extract the string `content` field well enough, but for real validation we hand off to `jq`: ```bash PAYLOAD='{"model":"apple-foundationmodel","messages":[{"role":"user","content":"Return JSON with fields chip, year, cores. Describe the Apple M1 chip. Return ONLY JSON."}],"response_format":{"type":"json_object"},"max_tokens":120}' curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" -d "$PAYLOAD" \ | awk 'BEGIN { RS="\"content\" :" } NR==2 { match($0, /"([^"\\]|\\.)*"/) s = substr($0, RSTART+1, RLENGTH-2) gsub(/\\n/, "\n", s); gsub(/\\"/, "\"", s); gsub(/\\\\/, "\\", s) print s }' \ | sed -E 's/^```(json)?//; s/```$//' \ | jq '.' ``` Real output: ```json { "chip": "Apple M1", "year": 2020, "cores": { "CPU": 8, "GPU": 8 } } ``` Lab script: [`03_json.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/awk/03_json.sh). ## 4. Error handling Use AWK to extract the `.error.message` string from the JSON body after curl gives you the HTTP status: ```bash tmp=$(mktemp) status=$(curl -sS -o "$tmp" -w '%{http_code}' \ http://localhost:11434/v1/embeddings \ -H "Content-Type: application/json" \ -d '{"model":"apple-foundationmodel","input":"apfel runs 100% on-device."}') if [[ "$status" -ge 400 ]]; then msg=$(awk 'BEGIN { RS="\"message\" :" } NR==2 { match($0, /"([^"\\]|\\.)*"/) s = substr($0, RSTART+1, RLENGTH-2) gsub(/\\"/, "\"", s); gsub(/\\\\/, "\\", s) print s }' "$tmp") echo "Got expected error: HTTP $status - ${msg:-see response}" fi rm -f "$tmp" ``` Real output: ```text Got expected error: HTTP 501 - Embeddings not supported by Apple's on-device model. ``` Lab script: [`04_errors.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/awk/04_errors.sh). ## 5. Tool calling (delegate to Bash) Tool calling requires constructing nested JSON with escaped strings, modifying the conversation, and posting back - this is outside AWK's sweet spot. The idiomatic AWK solution is to delegate to the Bash tool-calling script: ```bash #!/usr/bin/env bash here=$(cd "$(dirname "$0")" && pwd) bash "$here/../bash-curl/05_tools.sh" ``` Real output: ```text The current temperature in Vienna is 14 degrees Celsius. ``` Lab script: [`05_tools.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/awk/05_tools.sh). For tool-heavy code, reach for [python.md](python.md) or [nodejs.md](nodejs.md). ## 6. Real example - summarize stdin AWK does what AWK is good at - text cleanup - then hands the clean text to apfel: ```bash cleaned=$(awk ' { sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+$/, ""); gsub(/[[:space:]]+/, " ") } { print } ' | awk 'NF') payload=$(jq -n --arg text "$cleaned" '{ model:"apple-foundationmodel", messages:[ {role:"system", content:"You are a concise summarizer. Reply with one short paragraph."}, {role:"user", content:("Summarize:\n\n" + $text)} ], max_tokens:150 }') curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" -d "$payload" \ | awk 'BEGIN { RS="\"content\" :" } NR==2 { match($0, /"([^"\\]|\\.)*"/) s = substr($0, RSTART+1, RLENGTH-2) gsub(/\\n/, "\n", s); gsub(/\\"/, "\"", s); gsub(/\\\\/, "\\", s) print s }' ``` Real output: ```text The Apple M1 chip, launched in November 2020, marked Apple's first ARM-based system-on-a-chip for Macs. This chip features an 8-core CPU with four performance and four efficiency cores, along with an integrated GPU capable of up to 8 cores. By consolidating the CPU, GPU, memory, and neural engine on a single die, the M1 chip achieved notable performance-per-watt improvements compared to its Intel counterparts. ``` Lab script: [`06_example.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/awk/06_example.sh). ## Troubleshooting - **AWK doesn't parse JSON** - true, and we don't pretend it does. Use AWK for the `content` string extraction and delegate nested work to `jq`. - **Escaped characters leaking through** - order matters: unescape `\\n` first, then `\\"`, then `\\\\`, as shown in the `gsub` chain. - **macOS `awk` vs `gawk`** - the scripts above use only POSIX AWK features that work with the system BSD `awk`. ## Tested with - apfel v1.0.3 / macOS 26.3.1 Apple Silicon (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - BSD awk 20200816 (system) / jq 1.7 / curl - Date: 2026-04-16 Runnable tests: [tests/test_awk.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_awk.py). ## See also [bash-curl.md](bash-curl.md), [perl.md](perl.md), [zsh.md](zsh.md), [apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) --- ### Guides/Bash Curl # How to use the Apple Foundation Model from Bash / curl Call Apple's on-device Foundation Model from plain Bash with `curl` and `jq` - no SDK, no dependencies beyond what's already on macOS. 100% on-device, zero API cost. Perfect for CI pipelines, one-liners, and quick smoke tests. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/bash-curl](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/bash-curl). ## Prerequisites - macOS 26+ Tahoe, Apple Silicon, Apple Intelligence enabled - `brew install apfel jq` (`curl` and `bash` already on every Mac) - `apfel --serve` running (port `11434`) ## 1. One-shot ```bash curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "apple-foundationmodel", "messages": [{"role": "user", "content": "In one sentence, what is the Swift programming language?"}], "max_tokens": 80 }' \ | jq -r '.choices[0].message.content' ``` Real output: ```text Swift is a modern, high-performance programming language developed by Apple for developing iOS, macOS, watchOS, and tvOS applications. ``` Lab script: [`01_oneshot.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/bash-curl/01_oneshot.sh). ## 2. Streaming ```bash curl -sS -N http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"apple-foundationmodel","messages":[{"role":"user","content":"List three Apple silicon chips, one per line."}],"max_tokens":80,"stream":true}' \ | while IFS= read -r line; do line="${line#data: }" [[ -z "$line" || "$line" == "[DONE]" ]] && continue content=$(printf '%s' "$line" | jq -r '.choices[0].delta.content // empty' 2>/dev/null || true) [[ -n "$content" ]] && printf '%s' "$content" done echo ``` Real output: ```text Here are three Apple silicon chips: - M1 - M2 - M3 ``` Lab script: [`02_stream.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/bash-curl/02_stream.sh). ## 3. JSON mode ```bash raw=$(curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "apple-foundationmodel", "messages": [{"role": "user", "content": "Return JSON with fields chip, year, cores. Describe the Apple M1 chip. Return ONLY JSON."}], "response_format": {"type": "json_object"}, "max_tokens": 120 }' \ | jq -r '.choices[0].message.content') raw=$(printf '%s' "$raw" | sed -E 's/^```(json)?//; s/```$//' | tr -d '\r') printf '%s' "$raw" | jq '.' ``` Real output: ```json { "chip": "Apple M1", "year": 2020, "cores": { "CPU": { "Cores": 8, "Threads": 8 }, "GPU": { "Cores": 16 } } } ``` Lab script: [`03_json.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/bash-curl/03_json.sh). ## 4. Error handling Capture HTTP status with `-w '%{http_code}'`: ```bash tmp=$(mktemp) http_status=$(curl -sS -o "$tmp" -w '%{http_code}' \ http://localhost:11434/v1/embeddings \ -H "Content-Type: application/json" \ -d '{"model":"apple-foundationmodel","input":"apfel runs 100% on-device."}') if [[ "$http_status" -ge 400 ]]; then msg=$(jq -r '.error.message // empty' "$tmp" 2>/dev/null || true) echo "Got expected error: HTTP $http_status - ${msg:-see response}" fi rm -f "$tmp" ``` Real output: ```text Got expected error: HTTP 501 - Embeddings not supported by Apple's on-device model. ``` Lab script: [`04_errors.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/bash-curl/04_errors.sh). ## 5. Tool calling Raw curl round-trip: model asks for a tool call, we answer, model replies: ```bash tools='[{ "type":"function", "function":{ "name":"get_weather", "description":"Get the current temperature in Celsius for a city.", "parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]} } }]' first=$(jq -n --argjson tools "$tools" '{ model: "apple-foundationmodel", messages: [{role:"user", content:"What is the temperature in Vienna right now?"}], tools: $tools, max_tokens: 256 }' | curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" -d @-) msg=$(jq -c '.choices[0].message' <<<"$first") call=$(jq -c '.tool_calls[0]' <<<"$msg") city=$(jq -r '.function.arguments | fromjson | .city' <<<"$call") tool_result=$(jq -cn --arg c "$city" --argjson t 14 '{city:$c, temp_c:$t}') tool_msg=$(jq -cn --arg id "$(jq -r '.id' <<<"$call")" --arg content "$tool_result" \ '{role:"tool", tool_call_id:$id, content:$content}') final_payload=$(jq -n --argjson msg "$msg" --argjson tool "$tool_msg" '{ model:"apple-foundationmodel", messages:[{role:"user",content:"What is the temperature in Vienna right now?"}, $msg, $tool], max_tokens:120 }') curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" -d "$final_payload" \ | jq -r '.choices[0].message.content' ``` Real output: ```text The current temperature in Vienna is 14 degrees Celsius. ``` Lab script: [`05_tools.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/bash-curl/05_tools.sh). ## 6. Real example - summarize a file ```bash text=$(cat "$1") payload=$(jq -n --arg text "$text" '{ model:"apple-foundationmodel", messages:[ {role:"system", content:"You are a concise summarizer. Reply with one short paragraph."}, {role:"user", content: ("Summarize:\n\n" + $text)} ], max_tokens:150 }') curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" -d "$payload" \ | jq -r '.choices[0].message.content' ``` Usage: `bash 06_example.sh README.md`. Real output: ```text The Apple M1 chip, released in November 2020, was Apple's first ARM-based system-on-a-chip for Mac computers. It features an 8-core CPU with four performance and four efficiency cores, plus an integrated GPU with up to 8 cores. The chip unified CPU, GPU, memory, and neural engine on a single die, delivering significant performance-per-watt improvements over the Intel chips it replaced. ``` Lab script: [`06_example.sh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/bash-curl/06_example.sh). ## Troubleshooting - **`jq: command not found`** - `brew install jq`. - **Empty streamed output** - the OpenAI SSE format prefixes each line with `data: ` - don't forget to strip it. - **Hard to escape JSON** - use `jq -n --arg text "..."` to build payloads; never concatenate strings. ## Tested with - apfel v1.0.3 / macOS 26.3.1 Apple Silicon (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - Bash 5.3 / jq 1.7 / curl (system) - Date: 2026-04-16 Runnable tests: [tests/test_bash_curl.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_bash_curl.py). ## See also [zsh.md](zsh.md), [python.md](python.md), [nodejs.md](nodejs.md), [apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) --- ### Guides/Index # Use Apple's Foundation Model from Any Scripting Language apfel exposes Apple's on-device **Foundation Model** over a local OpenAI-compatible HTTP server. Any language that can `POST` JSON to `http://localhost:11434/v1/chat/completions` can use it - **100% on-device, zero API cost, no network required for inference**. These guides show how, in the idioms each language uses. Every code block was run against a live `apfel --serve` before publishing. ## Guides | Language | Guide | Typical user | |----------|-------|--------------| | Python | [python.md](python.md) | AI/ML scripts, data tools, backends | | Node.js (JavaScript / TypeScript) | [nodejs.md](nodejs.md) | Web tools, CLI apps, desktop | | Ruby | [ruby.md](ruby.md) | Rails integrations, scripting | | PHP | [php.md](php.md) | Web apps, WordPress plugins | | Bash / curl | [bash-curl.md](bash-curl.md) | CI pipelines, one-liners, quick tests | | Zsh | [zsh.md](zsh.md) | Default macOS shell scripts | | AppleScript | [applescript.md](applescript.md) | Shortcuts, Automator, system automation | | Swift scripting | [swift-scripting.md](swift-scripting.md) | Native macOS scripting with URLSession | | Perl | [perl.md](perl.md) | Text pipelines (ships with macOS) | | AWK | [awk.md](awk.md) | Log/text processing with curl | ## What you need 1. **macOS 26+ Tahoe** on Apple Silicon 2. **Apple Intelligence enabled** (`System Settings -> Apple Intelligence & Siri`) 3. `brew install apfel` 4. `apfel --serve` running in a terminal (default port `11434`) ## Verify with curl ```bash apfel --serve & curl -s http://localhost:11434/health # {"status":"ok"} ``` If `/health` responds, you're ready. Pick your language and follow the guide. ## Honest limits (same for all languages) - **Context window:** 4096 tokens on macOS 26, 8192 on macOS 27 (read at runtime; `apfel --model-info` prints the live value) - **Embeddings:** not supported (returns HTTP 501 - see each guide's error-handling section) - **Vision / audio:** not supported - **JSON mode:** supported via `response_format: {type: "json_object"}` - occasionally wrapped in markdown fences, so the guides show a one-line fence-strip pattern - **Streaming:** supported via `stream: true` - **Tool calling:** supported via OpenAI `tools` parameter ## See the tests that produced these guides Every guide links to its exact test script and captured output on [Arthur-Ficial/apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab). If you want to rerun them on your own Mac: ```bash git clone https://github.com/Arthur-Ficial/apfel-guides-lab cd apfel-guides-lab apfel --serve & python3 -m pytest -v ``` --- ### Guides/Nodejs # How to use the Apple Foundation Model from Node.js Call Apple's on-device Foundation Model from Node.js using the official `openai` npm package, pointed at a local `apfel --serve`. 100% on-device, zero API cost. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/nodejs](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/nodejs). ## Prerequisites - macOS 26+ Tahoe, Apple Silicon, Apple Intelligence enabled - `brew install apfel` - `apfel --serve` running (port `11434`) - Node.js 20+ - `npm install openai` - `"type": "module"` in `package.json` (or use `.mjs` files) ## 1. One-shot chat completion ```js import OpenAI from "openai"; const client = new OpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "not-needed" }); const response = await client.chat.completions.create({ model: "apple-foundationmodel", messages: [{ role: "user", content: "In one sentence, what is the Swift programming language?" }], max_tokens: 80, }); console.log((response.choices[0].message.content || "").trim()); ``` Real output: ```text Swift is a modern, high-performance, and easy-to-learn programming language developed by Apple for building applications on iOS, macOS, watchOS, and tvOS. ``` Lab script: [`01_oneshot.mjs`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/nodejs/01_oneshot.mjs). ## 2. Streaming ```js import OpenAI from "openai"; const client = new OpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "not-needed" }); const stream = await client.chat.completions.create({ model: "apple-foundationmodel", messages: [{ role: "user", content: "List three Apple silicon chips, one per line." }], max_tokens: 80, stream: true, }); for await (const chunk of stream) { if (!chunk.choices || chunk.choices.length === 0) continue; const delta = chunk.choices[0].delta?.content ?? ""; process.stdout.write(delta); } process.stdout.write("\n"); ``` Real output: ```text Apple M1 Apple M2 Apple M2 Pro ``` Lab script: [`02_stream.mjs`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/nodejs/02_stream.mjs). ## 3. JSON mode ```js import OpenAI from "openai"; const client = new OpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "not-needed" }); const response = await client.chat.completions.create({ model: "apple-foundationmodel", messages: [{ role: "user", content: "Return JSON with fields 'chip', 'year', 'cores'. Describe the Apple M1 chip. Return ONLY JSON.", }], response_format: { type: "json_object" }, max_tokens: 120, }); let raw = (response.choices[0].message.content || "").trim(); raw = raw.replace(/^```(?:json)?\s*|\s*```$/gm, "").trim(); console.log(JSON.stringify(JSON.parse(raw), null, 2)); ``` Real output: ```json { "chip": "Apple M1", "year": 2020, "cores": { "CPU": 8, "GPU": 8 } } ``` Lab script: [`03_json.mjs`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/nodejs/03_json.mjs). ## 4. Error handling ```js import OpenAI from "openai"; const client = new OpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "not-needed" }); try { await client.embeddings.create({ model: "apple-foundationmodel", input: "apfel runs 100% on-device.", }); } catch (err) { if (err instanceof OpenAI.APIError) { console.log(`Got expected error: HTTP ${err.status} - ${err.message}`); } else { throw err; } } ``` Real output: ```text Got expected error: HTTP 501 - 501 Embeddings not supported by Apple's on-device model. ``` Lab script: [`04_errors.mjs`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/nodejs/04_errors.mjs). ## 5. Tool calling ```js import OpenAI from "openai"; const client = new OpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "not-needed" }); const tools = [{ type: "function", function: { name: "get_weather", description: "Get the current temperature in Celsius for a city.", parameters: { type: "object", properties: { city: { type: "string", description: "City name" } }, required: ["city"], }, }, }]; function getWeather({ city }) { const fake = { Vienna: 14, Cupertino: 19, Tokyo: 11 }; return JSON.stringify({ city, temp_c: fake[city] ?? 15 }); } const messages = [{ role: "user", content: "What is the temperature in Vienna right now?" }]; const first = await client.chat.completions.create({ model: "apple-foundationmodel", messages, tools, max_tokens: 256, }); const msg = first.choices[0].message; messages.push(msg); if (msg.tool_calls?.length) { for (const call of msg.tool_calls) { const args = JSON.parse(call.function.arguments); messages.push({ role: "tool", tool_call_id: call.id, content: getWeather(args) }); } const final = await client.chat.completions.create({ model: "apple-foundationmodel", messages, max_tokens: 120, }); console.log((final.choices[0].message.content || "").trim()); } ``` Real output: ```text The current temperature in Vienna is 14 degrees Celsius. ``` Lab script: [`05_tools.mjs`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/nodejs/05_tools.mjs). ## 6. Real example - summarize stdin ```js import OpenAI from "openai"; const text = await new Promise((resolve) => { let buf = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (c) => (buf += c)); process.stdin.on("end", () => resolve(buf.trim())); }); if (!text) { console.error("usage: cat file.txt | node 06_example.mjs"); process.exit(1); } const client = new OpenAI({ baseURL: "http://localhost:11434/v1", apiKey: "not-needed" }); const response = await client.chat.completions.create({ model: "apple-foundationmodel", messages: [ { role: "system", content: "You are a concise summarizer. Reply with one short paragraph." }, { role: "user", content: `Summarize:\n\n${text}` }, ], max_tokens: 150, }); console.log((response.choices[0].message.content || "").trim()); ``` Real output (M1 paragraph): ```text The Apple M1 chip, released in November 2020, was Apple's first ARM-based system-on-a-chip for Mac computers. It uses an 8-core CPU with four performance and four efficiency cores, plus an integrated GPU with up to 8 cores. The chip unified CPU, GPU, memory, and neural engine on a single die, delivering significant performance-per-watt improvements over the Intel chips it replaced. ``` Lab script: [`06_example.mjs`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/nodejs/06_example.mjs). ## Troubleshooting - **`ECONNREFUSED`** - start `apfel --serve` before running your Node script. - **Missing `choices[0]` during streaming** - handle the final usage chunk with the `if (!chunk.choices || chunk.choices.length === 0) continue;` guard above. - **TypeScript** - same code works; `npm install -D @types/node` for Node types. ## Tested with - apfel v1.0.3 / macOS 26.3.1 Apple Silicon (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - Node.js v25.8.1 / openai 4.x - Date: 2026-04-16 Runnable tests: [tests/test_nodejs.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_nodejs.py). ## See also [python.md](python.md), [ruby.md](ruby.md), [php.md](php.md), [bash-curl.md](bash-curl.md), [apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) --- ### Guides/Perl # How to use the Apple Foundation Model from Perl Call Apple's on-device Foundation Model from Perl using `HTTP::Tiny` + `JSON::PP` - both ship with the system Perl on macOS, so no CPAN needed. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/perl](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/perl). ## Prerequisites - macOS 26+ Tahoe, Apple Silicon, Apple Intelligence enabled - `brew install apfel` - `apfel --serve` running (port `11434`) - Perl 5.34+ (ships with macOS at `/usr/bin/perl`) No `cpanm` required - `HTTP::Tiny` and `JSON::PP` are core modules. ## 1. One-shot ```perl #!/usr/bin/env perl use strict; use warnings; use HTTP::Tiny; use JSON::PP; my $body = encode_json({ model => 'apple-foundationmodel', messages => [{ role => 'user', content => 'In one sentence, what is the Swift programming language?' }], max_tokens => 80, }); my $res = HTTP::Tiny->new->request( POST => 'http://localhost:11434/v1/chat/completions', { headers => { 'Content-Type' => 'application/json' }, content => $body } ); die "HTTP $res->{status}: $res->{content}\n" unless $res->{success}; my $text = decode_json($res->{content})->{choices}[0]{message}{content} // ''; $text =~ s/^\s+|\s+$//g; print "$text\n"; ``` Real output: ```text Swift is a modern, safe, and efficient programming language developed by Apple for building user interfaces, server-side applications, and command-line tools. ``` Lab script: [`01_oneshot.pl`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/perl/01_oneshot.pl). ## 2. Streaming `HTTP::Tiny` supports streaming via `data_callback`: ```perl #!/usr/bin/env perl use strict; use warnings; use HTTP::Tiny; use JSON::PP; my $body = encode_json({ model => 'apple-foundationmodel', messages => [{ role => 'user', content => 'List three Apple silicon chips, one per line.' }], max_tokens => 80, stream => JSON::PP::true, }); my $buf = ''; my $cb = sub { my ($chunk) = @_; $buf .= $chunk; while ($buf =~ s/^(.*?)\r?\n//) { my $line = $1; next if $line !~ s/^data:\s*//; next if $line eq '' || $line eq '[DONE]'; my $obj = eval { decode_json($line) } or next; next unless $obj->{choices} && @{$obj->{choices}}; my $delta = $obj->{choices}[0]{delta}{content}; if (defined $delta) { STDOUT->autoflush(1); print $delta; } } }; HTTP::Tiny->new->request( POST => 'http://localhost:11434/v1/chat/completions', { headers => { 'Content-Type' => 'application/json' }, content => $body, data_callback => $cb } ); print "\n"; ``` Real output: ```text Apple M1 Apple M2 Apple M3 ``` Lab script: [`02_stream.pl`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/perl/02_stream.pl). ## 3. JSON mode ```perl my $res = HTTP::Tiny->new->request( POST => 'http://localhost:11434/v1/chat/completions', { headers => { 'Content-Type' => 'application/json' }, content => encode_json({ model => 'apple-foundationmodel', messages => [{ role => 'user', content => "Return JSON with fields chip, year, cores. Describe the Apple M1 chip. Return ONLY JSON." }], response_format => { type => 'json_object' }, max_tokens => 120, }) } ); my $raw = decode_json($res->{content})->{choices}[0]{message}{content} // ''; $raw =~ s/^\s*```(?:json)?//; $raw =~ s/```\s*$//; $raw =~ s/^\s+|\s+$//g; my $parsed = decode_json($raw); print JSON::PP->new->pretty->canonical->encode($parsed); ``` Real output: ```json { "chip" : "Apple M1", "cores" : { "cpu" : 8, "gpu" : 8 }, "year" : 2020 } ``` Lab script: [`03_json.pl`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/perl/03_json.pl). ## 4. Error handling ```perl my $res = HTTP::Tiny->new->request( POST => 'http://localhost:11434/v1/embeddings', { headers => { 'Content-Type' => 'application/json' }, content => encode_json({ model => 'apple-foundationmodel', input => 'apfel runs 100% on-device.' }) } ); if ($res->{status} >= 400) { my $msg = 'see response'; my $err = eval { decode_json($res->{content}) }; if ($err && ref $err eq 'HASH' && $err->{error}) { $msg = $err->{error}{message} // $msg; } print "Got expected error: HTTP $res->{status} - $msg\n"; } ``` Real output: ```text Got expected error: HTTP 501 - Embeddings not supported by Apple's on-device model. ``` Lab script: [`04_errors.pl`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/perl/04_errors.pl). ## 5. Tool calling Full round-trip; see [`05_tools.pl`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/perl/05_tools.pl) for the complete script. Key snippet: ```perl binmode STDOUT, ':encoding(UTF-8)'; # avoid issues with °C, EUR etc. my $TOOLS = [{ type => 'function', function => { name => 'get_weather', description => 'Get the current temperature in Celsius for a city.', parameters => { type => 'object', properties => { city => { type => 'string' } }, required => ['city'] }, }, }]; # first call with tools, check $msg->{tool_calls}, answer, second call for final reply ``` Real output: ```text The current temperature in Vienna is 14°C. ``` ## 6. Real example - summarize stdin ```perl my $text = do { local $/; }; $text //= ''; $text =~ s/^\s+|\s+$//g; die "usage: cat file.txt | perl 06_example.pl\n" unless length $text; my $body = encode_json({ model => 'apple-foundationmodel', messages => [ { role => 'system', content => 'You are a concise summarizer. Reply with one short paragraph.' }, { role => 'user', content => "Summarize:\n\n$text" }, ], max_tokens => 150, }); my $res = HTTP::Tiny->new->request( POST => 'http://localhost:11434/v1/chat/completions', { headers => { 'Content-Type' => 'application/json' }, content => $body } ); my $content = decode_json($res->{content})->{choices}[0]{message}{content} // ''; $content =~ s/^\s+|\s+$//g; print "$content\n"; ``` Real output: ```text The Apple M1 chip, released in November 2020, was Apple's first ARM-based system-on-a-chip for Mac computers. It uses an 8-core CPU with four performance and four efficiency cores, plus an integrated GPU with up to 8 cores. The chip unified CPU, GPU, memory, and neural engine on a single die, delivering significant performance-per-watt improvements over the Intel chips it replaced. ``` Lab script: [`06_example.pl`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/perl/06_example.pl). ## Troubleshooting - **UTF-8 garbage on terminal** - `binmode STDOUT, ':encoding(UTF-8)';` at the top of the script. macOS's system Perl doesn't set this by default. - **Want LWP::UserAgent instead** - both work; HTTP::Tiny keeps the dependency footprint at zero on macOS. - **Streaming buffered** - make sure you do `STDOUT->autoflush(1)` inside the callback. ## Tested with - apfel v1.0.3 / macOS 26.3.1 Apple Silicon (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - Perl 5.34.1 (system) / HTTP::Tiny 0.076 / JSON::PP 4.06 - Date: 2026-04-16 Runnable tests: [tests/test_perl.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_perl.py). ## See also [python.md](python.md), [ruby.md](ruby.md), [bash-curl.md](bash-curl.md), [awk.md](awk.md), [apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) --- ### Guides/Php # How to use the Apple Foundation Model from PHP Call Apple's on-device Foundation Model from PHP using `openai-php/client`, pointed at a local `apfel --serve`. 100% on-device, zero API cost. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/php](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/php). ## Prerequisites - macOS 26+ Tahoe, Apple Silicon, Apple Intelligence enabled - `brew install apfel` - `apfel --serve` running (port `11434`) - PHP 8.1+ and Composer (`brew install php composer`) - `composer require openai-php/client guzzlehttp/guzzle` > `openai-php/client` needs a PSR-18 HTTP client; Guzzle is the usual pick. ## 1. One-shot ```php withBaseUri("http://localhost:11434/v1") ->withApiKey("not-needed") ->make(); $response = $client->chat()->create([ "model" => "apple-foundationmodel", "messages" => [ ["role" => "user", "content" => "In one sentence, what is the Swift programming language?"], ], "max_tokens" => 80, ]); echo trim($response->choices[0]->message->content ?? "") . "\n"; ``` Real output: ```text Swift is a modern, open-source programming language developed by Apple for developing software on platforms like iOS, macOS, watchOS, and tvOS, known for its safety, performance, and simplicity. ``` Lab script: [`01_oneshot.php`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/php/01_oneshot.php). ## 2. Streaming Use `createStreamed` and `foreach`: ```php withBaseUri("http://localhost:11434/v1")->withApiKey("not-needed")->make(); $stream = $client->chat()->createStreamed([ "model" => "apple-foundationmodel", "messages" => [["role" => "user", "content" => "List three Apple silicon chips, one per line."]], "max_tokens" => 80, ]); foreach ($stream as $response) { if (empty($response->choices)) continue; echo $response->choices[0]->delta->content ?? ""; flush(); } echo "\n"; ``` Real output: ```text Apple M1 Apple M2 Apple M2 Pro ``` Lab script: [`02_stream.php`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/php/02_stream.php). ## 3. JSON mode ```php withBaseUri("http://localhost:11434/v1")->withApiKey("not-needed")->make(); $response = $client->chat()->create([ "model" => "apple-foundationmodel", "messages" => [["role" => "user", "content" => "Return JSON with fields 'chip', 'year', 'cores'. Describe the Apple M1 chip. Return ONLY JSON."]], "response_format" => ["type" => "json_object"], "max_tokens" => 120, ]); $raw = trim($response->choices[0]->message->content ?? ""); $raw = preg_replace('/\A```(?:json)?\s*|\s*```\z/m', "", $raw); $data = json_decode(trim($raw), true, flags: JSON_THROW_ON_ERROR); echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; ``` Real output: ```json { "chip": "Apple M1", "year": 2020, "cores": { "cpu": 8, "gpu": 8 } } ``` Lab script: [`03_json.php`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/php/03_json.php). ## 4. Error handling ```php withBaseUri("http://localhost:11434/v1")->withApiKey("not-needed")->make(); try { $client->embeddings()->create([ "model" => "apple-foundationmodel", "input" => "apfel runs 100% on-device.", ]); } catch (ErrorException $e) { echo "Got expected error (HTTP 501): {$e->getMessage()}\n"; } ``` Real output: ```text Got expected error (HTTP 501): Embeddings not supported by Apple's on-device model. ``` Lab script: [`04_errors.php`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/php/04_errors.php). ## 5. Tool calling ```php withBaseUri("http://localhost:11434/v1")->withApiKey("not-needed")->make(); $tools = [[ "type" => "function", "function" => [ "name" => "get_weather", "description" => "Get the current temperature in Celsius for a city.", "parameters" => [ "type" => "object", "properties" => ["city" => ["type" => "string"]], "required" => ["city"], ], ], ]]; function get_weather(array $args): string { $fake = ["Vienna" => 14, "Cupertino" => 19, "Tokyo" => 11]; $city = $args["city"] ?? ""; return json_encode(["city" => $city, "temp_c" => $fake[$city] ?? 15]); } $messages = [["role" => "user", "content" => "What is the temperature in Vienna right now?"]]; $first = $client->chat()->create([ "model" => "apple-foundationmodel", "messages" => $messages, "tools" => $tools, "max_tokens" => 256, ]); $msg = $first->choices[0]->message; $messages[] = $msg->toArray(); if (!empty($msg->toolCalls)) { foreach ($msg->toolCalls as $call) { $args = json_decode($call->function->arguments, true) ?? []; $messages[] = ["role" => "tool", "tool_call_id" => $call->id, "content" => get_weather($args)]; } $final = $client->chat()->create([ "model" => "apple-foundationmodel", "messages" => $messages, "max_tokens" => 120, ]); echo trim($final->choices[0]->message->content ?? "") . "\n"; } ``` Real output: ```text The current temperature in Vienna is 15°C. ``` Lab script: [`05_tools.php`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/php/05_tools.php). ## 6. Real example - summarize stdin ```php withBaseUri("http://localhost:11434/v1")->withApiKey("not-needed")->make(); $response = $client->chat()->create([ "model" => "apple-foundationmodel", "messages" => [ ["role" => "system", "content" => "You are a concise summarizer. Reply with one short paragraph."], ["role" => "user", "content" => "Summarize:\n\n$text"], ], "max_tokens" => 150, ]); echo trim($response->choices[0]->message->content ?? "") . "\n"; ``` Real output: ```text Apple's M1 chip, released in November 2020, was Apple's first ARM-based system-on-a-chip for Mac computers. It uses an 8-core CPU with four performance and four efficiency cores, plus an integrated GPU with up to 8 cores. The chip unified CPU, GPU, memory, and neural engine on a single die, delivering significant performance-per-watt improvements over the Intel chips it replaced. ``` Lab script: [`06_example.php`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/php/06_example.php). ## Troubleshooting - **`No PSR-18 clients found`** - `composer require guzzlehttp/guzzle`. - **TLS / SSL errors** - make sure your `baseUri` starts with `http://`, not `https://`. - **Laravel / Symfony** - works inside any container; register `OpenAI::factory()->make()` as a singleton pointed at `APFEL_BASE_URL`. ## Tested with - apfel v1.0.3 / macOS 26.3.1 Apple Silicon (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - PHP 8.5.5 / openai-php/client 0.10.3 / Guzzle - Date: 2026-04-16 Runnable tests: [tests/test_php.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_php.py). ## See also [python.md](python.md), [nodejs.md](nodejs.md), [ruby.md](ruby.md), [bash-curl.md](bash-curl.md), [apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) --- ### Guides/Python # How to use the Apple Foundation Model from Python Call Apple's on-device Foundation Model from Python using the official `openai` SDK, pointed at a local `apfel --serve`. 100% on-device, zero API cost, no network required for inference. This guide covers the canonical patterns: one-shot completion, streaming, JSON mode, error handling, tool calling, and a real text-summarization example. Every code block was run against a live apfel server; the output below each snippet is real unedited stdout. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/python](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/python). ## Prerequisites - macOS 26+ Tahoe, Apple Silicon, Apple Intelligence enabled - `brew install apfel` - `apfel --serve` running (default port `11434`) - Python 3.11+ - `pip install openai` (or `uv add openai`) ## 1. One-shot chat completion Point the `openai` SDK at your local apfel server and call `chat.completions.create`: ```python from openai import OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="not-needed") response = client.chat.completions.create( model="apple-foundationmodel", messages=[ {"role": "user", "content": "In one sentence, what is the Swift programming language?"}, ], max_tokens=80, ) print((response.choices[0].message.content or "").strip()) ``` Real output: ```text Swift is a modern, high-performance, and safe programming language developed by Apple for developing iOS, macOS, watchOS, and tvOS applications. ``` Lab script: [`01_oneshot.py`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/python/01_oneshot.py). ## 2. Streaming Pass `stream=True` and iterate. Guard against empty `choices` on the final usage chunk: ```python import sys from openai import OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="not-needed") stream = client.chat.completions.create( model="apple-foundationmodel", messages=[{"role": "user", "content": "List three Apple silicon chips, one per line."}], max_tokens=80, stream=True, ) for chunk in stream: if not chunk.choices: continue delta = chunk.choices[0].delta.content or "" sys.stdout.write(delta) sys.stdout.flush() print() ``` Real output: ```text Apple M1 Apple M2 Apple M3 ``` Lab script: [`02_stream.py`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/python/02_stream.py). ## 3. JSON mode / structured output Request `response_format: {"type": "json_object"}` and parse. apfel may wrap output in markdown fences - the fence-strip regex below handles both cases cleanly: ```python import json, re from openai import OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="not-needed") response = client.chat.completions.create( model="apple-foundationmodel", messages=[{ "role": "user", "content": "Return JSON with fields 'chip', 'year', 'cores'. Describe the Apple M1 chip. Return ONLY JSON.", }], response_format={"type": "json_object"}, max_tokens=120, ) raw = (response.choices[0].message.content or "").strip() raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.MULTILINE).strip() data = json.loads(raw) print(json.dumps(data, indent=2, sort_keys=True)) ``` Real output: ```json { "chip": "Apple M1", "cores": 8, "year": 2020 } ``` Lab script: [`03_json.py`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/python/03_json.py). ## 4. Error handling apfel returns honest HTTP errors for unsupported features. Embeddings return `501`: ```python from openai import APIStatusError, OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="not-needed") try: client.embeddings.create( model="apple-foundationmodel", input="apfel runs 100% on-device.", ) except APIStatusError as e: print(f"Got expected error: HTTP {e.status_code} - {e.message}") ``` Real output: ```text Got expected error: HTTP 501 - Error code: 501 - {'error': {'message': "Embeddings not supported by Apple's on-device model.", 'type': 'invalid_request_error'}} ``` Lab script: [`04_errors.py`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/python/04_errors.py). ## 5. Tool calling Define a tool schema, send a prompt, handle the tool call, post the result, get the final answer: ```python import json from openai import OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="not-needed") TOOLS = [{ "type": "function", "function": { "name": "get_weather", "description": "Get the current temperature in Celsius for a city.", "parameters": { "type": "object", "properties": {"city": {"type": "string", "description": "City name"}}, "required": ["city"], }, }, }] def get_weather(city: str, **_: object) -> str: fake = {"Vienna": 14, "Cupertino": 19, "Tokyo": 11} return json.dumps({"city": city, "temp_c": fake.get(city, 15)}) messages = [{"role": "user", "content": "What is the temperature in Vienna right now?"}] first = client.chat.completions.create( model="apple-foundationmodel", messages=messages, tools=TOOLS, max_tokens=256, ) msg = first.choices[0].message messages.append(msg.model_dump(exclude_none=True)) if msg.tool_calls: for call in msg.tool_calls: args = json.loads(call.function.arguments) result = get_weather(**args) messages.append({"role": "tool", "tool_call_id": call.id, "content": result}) final = client.chat.completions.create( model="apple-foundationmodel", messages=messages, max_tokens=120, ) print((final.choices[0].message.content or "").strip()) ``` Real output: ```text The current temperature in Vienna is 14°C. ``` Lab script: [`05_tools.py`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/python/05_tools.py). ## 6. Real example - summarize a file from stdin ```python import sys from openai import OpenAI text = sys.stdin.read().strip() if not text: sys.exit("usage: cat file.txt | python 06_example.py") client = OpenAI(base_url="http://localhost:11434/v1", api_key="not-needed") response = client.chat.completions.create( model="apple-foundationmodel", messages=[ {"role": "system", "content": "You are a concise summarizer. Reply with one short paragraph."}, {"role": "user", "content": f"Summarize:\n\n{text}"}, ], max_tokens=150, ) print((response.choices[0].message.content or "").strip()) ``` ```bash cat README.md | python 06_example.py ``` Real output (summarizing a paragraph about the M1 chip): ```text The Apple M1 chip, released in November 2020, was Apple's first ARM-based system-on-a-chip for Mac computers. It features an 8-core CPU with four performance and four efficiency cores, plus an integrated GPU with up to 8 cores. The chip combines CPU, GPU, memory, and neural engine on a single die, delivering significant performance-per-watt improvements over the Intel chips it replaced. ``` Lab script: [`06_example.py`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/python/06_example.py). ## Troubleshooting - **`Connection refused` on port 11434** - run `apfel --serve` first. - **`Embeddings not supported`** - apfel is text-only; use sentence-transformers or another embedder for vectors. - **`JSONDecodeError` in JSON mode** - keep the fence-strip regex; apfel sometimes wraps JSON in `` ```json ... ``` ``. - **Empty streaming output** - make sure your client handles the final `usage` chunk with empty `choices`. The `if not chunk.choices: continue` above covers it. - **Model refuses a tool call** - small on-device models occasionally decline. Retry the whole call. ## Tested with - apfel v1.0.3 (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - macOS 26.3.1, Apple Silicon - Python 3.11 / openai 2.31.0 - Date: 2026-04-16 Full runnable test suite + captured outputs: [apfel-guides-lab/tests/test_python.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_python.py). ## See also - [nodejs.md](nodejs.md) - same thing from Node.js - [ruby.md](ruby.md) / [php.md](php.md) - same thing from Ruby / PHP - [bash-curl.md](bash-curl.md) - raw HTTP, no SDK - [Arthur-Ficial/apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) - runnable proof for all ten languages --- ### Guides/Ruby # How to use the Apple Foundation Model from Ruby Call Apple's on-device Foundation Model from Ruby using the `ruby-openai` gem, pointed at a local `apfel --serve`. 100% on-device, zero API cost. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/ruby](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/ruby). ## Prerequisites - macOS 26+ Tahoe, Apple Silicon, Apple Intelligence enabled - `brew install apfel` - `apfel --serve` running (port `11434`) - Ruby 2.6+ (ships with macOS) - `gem install ruby-openai` (or `bundle add ruby-openai`) ## 1. One-shot ```ruby require "openai" client = OpenAI::Client.new( uri_base: "http://localhost:11434", access_token: "not-needed" ) response = client.chat( parameters: { model: "apple-foundationmodel", messages: [{ role: "user", content: "In one sentence, what is the Swift programming language?" }], max_tokens: 80 } ) puts response.dig("choices", 0, "message", "content").strip ``` Real output: ```text Swift is a modern, high-performance, and versatile programming language designed for developing iOS, macOS, watchOS, and tvOS applications. ``` Lab script: [`01_oneshot.rb`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/ruby/01_oneshot.rb). ## 2. Streaming `ruby-openai` streams via a callback `proc`: ```ruby require "openai" client = OpenAI::Client.new(uri_base: "http://localhost:11434", access_token: "not-needed") client.chat( parameters: { model: "apple-foundationmodel", messages: [{ role: "user", content: "List three Apple silicon chips, one per line." }], max_tokens: 80, stream: proc do |chunk, _bytesize| next if chunk.dig("choices").nil? || chunk["choices"].empty? piece = chunk.dig("choices", 0, "delta", "content") print piece if piece $stdout.flush end } ) puts ``` Real output: ```text Apple M1 Apple M2 Apple M3 ``` Lab script: [`02_stream.rb`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/ruby/02_stream.rb). ## 3. JSON mode ```ruby require "openai" require "json" client = OpenAI::Client.new(uri_base: "http://localhost:11434", access_token: "not-needed") response = client.chat( parameters: { model: "apple-foundationmodel", messages: [{ role: "user", content: "Return JSON with fields 'chip', 'year', 'cores'. Describe the Apple M1 chip. Return ONLY JSON." }], response_format: { type: "json_object" }, max_tokens: 120 } ) raw = response.dig("choices", 0, "message", "content").to_s.strip raw = raw.sub(/\A```(?:json)?\s*/, "").sub(/\s*```\z/, "").strip puts JSON.pretty_generate(JSON.parse(raw)) ``` Real output: ```json { "chip": "Apple M1", "year": 2020, "cores": { "cpu": 8, "gpu": 8 } } ``` Lab script: [`03_json.rb`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/ruby/03_json.rb). ## 4. Error handling `ruby-openai` surfaces HTTP errors via `Faraday::Error`: ```ruby require "openai" client = OpenAI::Client.new(uri_base: "http://localhost:11434", access_token: "not-needed") begin client.embeddings(parameters: { model: "apple-foundationmodel", input: "apfel runs 100% on-device." }) rescue Faraday::Error => e status = e.response && e.response[:status] puts "Got expected error: HTTP #{status} - #{e.message}" end ``` Real output: ```text Got expected error: HTTP 501 - the server responded with status 501 ``` Lab script: [`04_errors.rb`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/ruby/04_errors.rb). ## 5. Tool calling ```ruby require "openai" require "json" client = OpenAI::Client.new(uri_base: "http://localhost:11434", access_token: "not-needed") TOOLS = [{ type: "function", function: { name: "get_weather", description: "Get the current temperature in Celsius for a city.", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } } }] def get_weather(args) fake = { "Vienna" => 14, "Cupertino" => 19, "Tokyo" => 11 } { city: args["city"], temp_c: fake[args["city"]] || 15 }.to_json end messages = [{ role: "user", content: "What is the temperature in Vienna right now?" }] first = client.chat(parameters: { model: "apple-foundationmodel", messages: messages, tools: TOOLS, max_tokens: 256 }) msg = first.dig("choices", 0, "message") messages << msg if msg["tool_calls"] && !msg["tool_calls"].empty? msg["tool_calls"].each do |call| args = JSON.parse(call.dig("function", "arguments")) messages << { role: "tool", tool_call_id: call["id"], content: get_weather(args) } end final = client.chat(parameters: { model: "apple-foundationmodel", messages: messages, max_tokens: 120 }) puts final.dig("choices", 0, "message", "content").to_s.strip end ``` Real output: ```text The current temperature in Vienna is 14 degrees Celsius. ``` Lab script: [`05_tools.rb`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/ruby/05_tools.rb). ## 6. Real example - summarize stdin ```ruby require "openai" text = $stdin.read.strip if text.empty? warn "usage: cat file.txt | ruby 06_example.rb" exit 1 end client = OpenAI::Client.new(uri_base: "http://localhost:11434", access_token: "not-needed") response = client.chat( parameters: { model: "apple-foundationmodel", messages: [ { role: "system", content: "You are a concise summarizer. Reply with one short paragraph." }, { role: "user", content: "Summarize:\n\n#{text}" } ], max_tokens: 150 } ) puts response.dig("choices", 0, "message", "content").to_s.strip ``` Real output: ```text The Apple M1 chip, launched in November 2020, marked Apple's first ARM-based system-on-a-chip for Mac computers. It features an 8-core CPU with four performance and four efficiency cores, along with an integrated GPU that can have up to 8 cores. The chip integrates CPU, GPU, memory, and neural engine on a single die, offering significant performance-per-watt improvements over its Intel predecessors. ``` Lab script: [`06_example.rb`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/ruby/06_example.rb). ## Troubleshooting - **Connection refused** - start `apfel --serve` before running the Ruby script. - **`e.response[:status]`** - only present when Faraday raises with a full response object. For low-level socket errors it's nil. - **Rails** - these patterns drop into a Rails controller or background job without changes. Use the same `OpenAI::Client` pointed at localhost. ## Tested with - apfel v1.0.3 / macOS 26.3.1 Apple Silicon (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - Ruby 2.6.10 (system) / ruby-openai 7.4.0 - Date: 2026-04-16 Runnable tests: [tests/test_ruby.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_ruby.py). ## See also [python.md](python.md), [nodejs.md](nodejs.md), [php.md](php.md), [bash-curl.md](bash-curl.md), [apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) --- ### Guides/Swift Scripting # How to use the Apple Foundation Model from Swift scripts Call Apple's on-device Foundation Model from a Swift script using `URLSession`. This is the "shell script written in Swift" pattern - fast, native, and pointed at a local `apfel --serve`. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/swift-scripting](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/swift-scripting). > For **in-app** use, you can skip apfel entirely and call `FoundationModels` directly via Apple's own Swift SDK. This guide is for **scripts** - anything you'd run with `swift path/to/file.swift` from the command line. ## Prerequisites - macOS 26+ Tahoe, Apple Silicon, Apple Intelligence enabled - `brew install apfel` - `apfel --serve` running (port `11434`) - Xcode Command Line Tools (`xcode-select --install`) - Swift 6 ships with the OS Swift scripts use `#!/usr/bin/env swift` or just `swift file.swift`. ## 1. One-shot ```swift #!/usr/bin/env swift import Foundation struct ChatRequest: Encodable { struct Message: Encodable { let role, content: String } let model: String let messages: [Message] let max_tokens: Int } struct ChatResponse: Decodable { struct Choice: Decodable { struct Msg: Decodable { let content: String? }; let message: Msg } let choices: [Choice] } var req = URLRequest(url: URL(string: "http://localhost:11434/v1/chat/completions")!) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONEncoder().encode(ChatRequest( model: "apple-foundationmodel", messages: [.init(role: "user", content: "In one sentence, what is the Swift programming language?")], max_tokens: 80 )) let sem = DispatchSemaphore(value: 0) var finalText = "" URLSession.shared.dataTask(with: req) { data, _, _ in defer { sem.signal() } guard let data = data, let decoded = try? JSONDecoder().decode(ChatResponse.self, from: data), let text = decoded.choices.first?.message.content else { return } finalText = text.trimmingCharacters(in: .whitespacesAndNewlines) }.resume() sem.wait() print(finalText) ``` Real output: ```text Swift is a modern, open-source programming language known for its safety features, ease of use, and performance, primarily used for developing iOS, macOS, watchOS, and tvOS applications. ``` Lab script: [`01_oneshot.swift`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/swift-scripting/01_oneshot.swift). ## 2. Streaming Use `URLSession.shared.bytes(for:)` and parse SSE lines as they arrive: ```swift #!/usr/bin/env swift import Foundation let body: [String: Any] = [ "model": "apple-foundationmodel", "messages": [["role": "user", "content": "List three Apple silicon chips, one per line."]], "max_tokens": 80, "stream": true, ] var req = URLRequest(url: URL(string: "http://localhost:11434/v1/chat/completions")!) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONSerialization.data(withJSONObject: body) let sem = DispatchSemaphore(value: 0) Task { defer { sem.signal() } let (bytes, _) = try await URLSession.shared.bytes(for: req) for try await line in bytes.lines { var payload = line if payload.hasPrefix("data: ") { payload.removeFirst("data: ".count) } guard !payload.isEmpty, payload != "[DONE]" else { continue } guard let data = payload.data(using: .utf8), let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let choices = obj["choices"] as? [[String: Any]], let delta = choices.first?["delta"] as? [String: Any], let content = delta["content"] as? String else { continue } FileHandle.standardOutput.write(content.data(using: .utf8) ?? Data()) } print() } sem.wait() ``` Real output: ```text Sure! Here are three Apple silicon chips: - M1 - M2 - M3 ``` Lab script: [`02_stream.swift`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/swift-scripting/02_stream.swift). ## 3. JSON mode ```swift let body: [String: Any] = [ "model": "apple-foundationmodel", "messages": [["role": "user", "content": "Return JSON with fields chip, year, cores. Describe the Apple M1 chip. Return ONLY JSON."]], "response_format": ["type": "json_object"], "max_tokens": 120, ] // ... (same URLSession dance as above) ... var stripped = rawContent.trimmingCharacters(in: .whitespacesAndNewlines) stripped = stripped.replacingOccurrences(of: "```json", with: "") .replacingOccurrences(of: "```", with: "") .trimmingCharacters(in: .whitespacesAndNewlines) let parsed = try JSONSerialization.jsonObject(with: Data(stripped.utf8)) let pretty = try JSONSerialization.data(withJSONObject: parsed, options: [.prettyPrinted, .sortedKeys]) print(String(data: pretty, encoding: .utf8) ?? "") ``` Real output: ```json { "chip" : "Apple M1", "cores" : { "CPU" : { "count" : 8, "type" : "High-performance" }, "GPU" : { "count" : 8, "type" : "High-efficiency" } }, "year" : 2020 } ``` Full script: [`03_json.swift`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/swift-scripting/03_json.swift). ## 4. Error handling Check the `HTTPURLResponse.statusCode`: ```swift URLSession.shared.dataTask(with: req) { data, response, _ in guard let http = response as? HTTPURLResponse else { return } if http.statusCode >= 400 { var msg = "see response" if let data = data, let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let err = obj["error"] as? [String: Any], let m = err["message"] as? String { msg = m } print("Got expected error: HTTP \(http.statusCode) - \(msg)") } }.resume() ``` Real output: ```text Got expected error: HTTP 501 - Embeddings not supported by Apple's on-device model. ``` Full script: [`04_errors.swift`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/swift-scripting/04_errors.swift). ## 5. Tool calling Standard OpenAI tool-calling round-trip via two `URLSession` POSTs. See the full script: [`05_tools.swift`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/swift-scripting/05_tools.swift). Real output: ```text The current temperature in Vienna is 14 degrees Celsius. ``` ## 6. Real example - summarize stdin ```swift let stdin = FileHandle.standardInput.readDataToEndOfFile() guard let text = String(data: stdin, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { exit(1) } let body: [String: Any] = [ "model": "apple-foundationmodel", "messages": [ ["role": "system", "content": "You are a concise summarizer. Reply with one short paragraph."], ["role": "user", "content": "Summarize:\n\n\(text)"], ], "max_tokens": 150, ] // URLSession POST as in example 1, print trimmed content ``` Real output: ```text Apple released their first ARM-based system-on-a-chip for Mac computers in November 2020. It has an 8-core CPU with four performance cores and four efficiency cores, plus an integrated GPU with up to 8 cores. It unified CPU, GPU, memory, and neural engine on a single die, delivering significant performance-per-watt improvements over the Intel chips it replaced. ``` Full script: [`06_example.swift`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/swift-scripting/06_example.swift). ## Troubleshooting - **`swift` is slow to start** - `swift file.swift` compiles on every run. For hot-loop scripts, compile once: `swiftc -O file.swift -o bin && ./bin`. - **Concurrency warnings on Swift 6** - the scripts use `DispatchSemaphore` to bridge async URLSession into a sync script. Inside an app, switch to `async/await` everywhere. - **Want tighter types** - define `Codable` structs for every response shape. The one-shot example above shows the pattern. ## Tested with - apfel v1.0.3 / macOS 26.3.1 Apple Silicon (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - Swift 6.3 (system) - Date: 2026-04-16 Runnable tests: [tests/test_swift_scripting.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_swift_scripting.py). ## See also [applescript.md](applescript.md), [python.md](python.md), [nodejs.md](nodejs.md), [apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) --- ### Guides/Zsh # How to use the Apple Foundation Model from Zsh Call Apple's on-device Foundation Model from Zsh - the default shell on modern macOS. Zsh's parameter expansion, associative arrays, and `print -r` make raw HTTP calls more concise than the Bash equivalent. Runnable scripts + tests: [Arthur-Ficial/apfel-guides-lab/scripts/zsh](https://github.com/Arthur-Ficial/apfel-guides-lab/tree/main/scripts/zsh). ## Prerequisites - macOS 26+ Tahoe (Zsh 5.9+ ships with the OS) - `brew install apfel jq` - `apfel --serve` running (port `11434`) ## 1. One-shot ```zsh #!/bin/zsh emulate -L zsh setopt err_exit pipe_fail no_unset local -A req=( model "apple-foundationmodel" prompt "In one sentence, what is the Swift programming language?" ) local payload="$(jq -cn --arg m "$req[model]" --arg p "$req[prompt]" \ '{model:$m, messages:[{role:"user", content:$p}], max_tokens:80}')" curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" -d "$payload" \ | jq -r '.choices[0].message.content' ``` Real output: ```text Swift is a modern, high-performance programming language developed by Apple for developing apps and systems on iOS, macOS, watchOS, and tvOS. ``` Lab script: [`01_oneshot.zsh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/zsh/01_oneshot.zsh). ## 2. Streaming ```zsh #!/bin/zsh emulate -L zsh setopt err_exit pipe_fail no_unset no_xtrace no_verbose curl -sS -N http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"apple-foundationmodel","messages":[{"role":"user","content":"List three Apple silicon chips, one per line."}],"max_tokens":80,"stream":true}' \ | while IFS= read -r line; do line=${line#data: } [[ -z $line || $line == "[DONE]" ]] && continue piece=$(print -r -- "$line" | jq -r '.choices[0].delta.content // empty' 2>/dev/null) || piece= [[ -n $piece ]] && print -rn -- "$piece" done print ``` Real output: ```text Apple M1 Apple M2 Apple M2 Pro ``` Lab script: [`02_stream.zsh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/zsh/02_stream.zsh). ## 3. JSON mode Zsh parameter expansion strips markdown fences without calling `sed`: ```zsh #!/bin/zsh emulate -L zsh setopt err_exit pipe_fail no_unset local raw raw=$(curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model":"apple-foundationmodel", "messages":[{"role":"user","content":"Return JSON with fields chip, year, cores. Describe the Apple M1 chip. Return ONLY JSON."}], "response_format":{"type":"json_object"}, "max_tokens":120 }' | jq -r '.choices[0].message.content') raw=${raw#\`\`\`json} raw=${raw#\`\`\`} raw=${raw%\`\`\`} raw=${raw//$'\r'/} print -r -- "$raw" | jq '.' ``` Real output: ```json { "chip": "Apple M1", "year": 2020, "cores": { "CPU": 8, "GPU": 8 } } ``` Lab script: [`03_json.zsh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/zsh/03_json.zsh). ## 4. Error handling ```zsh #!/bin/zsh emulate -L zsh setopt err_exit pipe_fail no_unset local tmp=$(mktemp) local http_status http_status=$(curl -sS -o "$tmp" -w '%{http_code}' \ http://localhost:11434/v1/embeddings \ -H "Content-Type: application/json" \ -d '{"model":"apple-foundationmodel","input":"apfel runs 100% on-device."}') if (( http_status >= 400 )); then local msg=$(jq -r '.error.message // empty' "$tmp" 2>/dev/null) || true print -r -- "Got expected error: HTTP ${http_status} - ${msg:-see response}" fi rm -f "$tmp" ``` Real output: ```text Got expected error: HTTP 501 - Embeddings not supported by Apple's on-device model. ``` Lab script: [`04_errors.zsh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/zsh/04_errors.zsh). ## 5. Tool calling ``` /* Detailed source-code truncated for AI context efficiency. */ ``` Real output: ```text The current temperature in Vienna is 14 degrees Celsius. ``` Lab script: [`05_tools.zsh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/zsh/05_tools.zsh). ## 6. Real example - summarize stdin ```zsh #!/bin/zsh emulate -L zsh setopt err_exit pipe_fail no_unset local text=$(cat) [[ -z $text ]] && { print -u 2 -- "usage: cat file.txt | zsh 06_example.zsh"; exit 1 } local payload=$(jq -n --arg text "$text" '{ model:"apple-foundationmodel", messages:[ {role:"system", content:"You are a concise summarizer. Reply with one short paragraph."}, {role:"user", content: ("Summarize:\n\n" + $text)} ], max_tokens:150 }') curl -sS http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" -d "$payload" \ | jq -r '.choices[0].message.content' ``` Real output: ```text The Apple M1 chip, released in 2020, was Apple's first ARM-based system-on-a-chip for Mac computers. It features an 8-core CPU with four performance and four efficiency cores, plus an integrated GPU with up to 8 cores, providing significant performance-per-watt improvements over Intel chips. ``` Lab script: [`06_example.zsh`](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/scripts/zsh/06_example.zsh). ## Troubleshooting - **`local piece` prints the assignment** - Zsh prints declarations when used outside functions with `no_unset`. Drop the `local` or wrap the block in a function. The streaming script above shows the clean pattern. - **Scripts using Bash heredocs don't work** - Zsh's quoting rules differ slightly. The scripts above use single-quoted payloads or `jq -n --arg` to sidestep it. ## Tested with - apfel v1.0.3 / macOS 26.3.1 Apple Silicon (original capture; the CLI and HTTP surfaces used here are release-gated by apfel's test suite on every version) - zsh 5.9 (system) / jq 1.7 - Date: 2026-04-16 Runnable tests: [tests/test_zsh.py](https://github.com/Arthur-Ficial/apfel-guides-lab/blob/main/tests/test_zsh.py). ## See also [bash-curl.md](bash-curl.md), [applescript.md](applescript.md), [swift-scripting.md](swift-scripting.md), [apfel-guides-lab](https://github.com/Arthur-Ficial/apfel-guides-lab) --- ### Integrations/Opencode # apfel + opencode Run [opencode](https://opencode.ai), the open-source terminal AI coding agent, against apfel's OpenAI-compatible server so every token stays on-device at zero cost. **Verified:** opencode 1.17.16 + apfel 1.8.2, macOS 26 (Apple Silicon). A real session transcript is at the bottom of this page. ## 0. Install opencode Use the official installer - it fetches the platform binary to `~/.opencode/bin/opencode`: ```bash curl -fsSL https://opencode.ai/install | bash ``` Then ensure `~/.opencode/bin` is on your `PATH`. > Gotcha: `npm install opencode-ai` on its own may not produce a working `opencode` command, because the package's post-install download is skipped under npm's `allow-scripts` policy. The `curl` installer above avoids that. ## 1. Start apfel ```bash apfel --serve ``` This serves the OpenAI API at `http://127.0.0.1:11434/v1`. Confirm it is up: ```bash curl -s http://127.0.0.1:11434/v1/models ``` ## 2. Configure opencode Write this to `~/.config/opencode/opencode.json`: ```json { "$schema": "https://opencode.ai/config.json", "compaction": { "auto": true, "prune": true, "reserved": 512 }, "default_agent": "lean", "agent": { "lean": { "mode": "primary", "model": "apfel/apple-foundationmodel", "prompt": "You are a concise assistant. Answer directly.", "permission": { "*": "deny" } } }, "provider": { "apfel": { "npm": "@ai-sdk/openai-compatible", "name": "apfel", "options": { "baseURL": "http://127.0.0.1:11434/v1", "apiKey": "not-needed" }, "models": { "apple-foundationmodel": { "name": "apple-foundationmodel" } } } } } ``` The model id `apple-foundationmodel` must match exactly what apfel reports at `/v1/models`. `apiKey` is a placeholder: a local apfel server started without `--serve-token` needs no auth, but opencode's OpenAI-compatible provider still wants the field present. ## 3. Run it One-shot (note the env var - see [the 4096-token fix](#the-4096-token-window-the-fix-you-must-set) below): ```bash OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1 opencode run --agent lean "In one sentence, what is a hash map?" ``` Interactive: ```bash OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1 opencode ``` Set that variable once in your shell profile (`~/.zshrc`) so you never forget it: ```bash echo 'export OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1' >> ~/.zshrc ``` ## The 4096-token window: the fix you must set apfel's on-device model has a **4096-token context window on macOS 26** (8192 on macOS 27 - apfel reads the real size at runtime; everything on this page was measured on macOS 26, and the failure mode is identical on macOS 27, just with more headroom). opencode is a full coding agent, and it **injects your instruction files into the system prompt on every request**. It loads them in this order (each category accumulates - they do not replace each other): 1. Local `AGENTS.md` / `CLAUDE.md` (walking up from the current directory) 2. Global `~/.config/opencode/AGENTS.md` 3. Claude Code fallback: **`~/.claude/CLAUDE.md`** That third one is the trap. opencode has undocumented Claude Code compatibility: if you use Claude Code, your global `~/.claude/CLAUDE.md` gets pasted into opencode's system prompt verbatim. A big one (this machine's was ~12 KB / ~3,300 tokens) fills the 4096-token window before you type a word, and apfel returns an honest HTTP 400: ``` Error: Input exceeds the model's context window. Shorten the conversation history. ``` ### The fix: disable the Claude Code prompt Set this environment variable. It tells opencode to stop loading `~/.claude/CLAUDE.md`: ```bash export OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1 ``` Proven on this machine, with the 12 KB `~/.claude/CLAUDE.md` left in place: | | Request to apfel | Result | |---|---|---| | Without the var | 13,461 bytes | **400 - context overflow** | | `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1` | 2,498 bytes | **200 OK, real answer** | The `instructions` field in `opencode.json` does **not** help here - it *adds* files, it cannot remove the auto-loaded `CLAUDE.md`. The env var is the fix. (To drop all Claude Code compatibility, not just the prompt, use `OPENCODE_DISABLE_CLAUDE_CODE=1`.) ### Then keep the rest of the payload small With `CLAUDE.md` out of the way, two things in the config above keep you comfortably inside 4096 tokens: - `"permission": { "*": "deny" }` on the `lean` agent stops opencode sending tool schemas (they eat the window fast). - A short custom `"prompt"` replaces opencode's default agent instructions. Also keep any **project** `AGENTS.md` small - it loads too, and the env var does not touch it. Because of that window, apfel is a great opencode backend for **short Q&A and small, focused edits** - not for large-repo, many-tool, long-running agent sessions. That is a property of the on-device model, not the wiring. The `apfel --count-tokens` flag (see [docs/cli-reference.md](../cli-reference.md)) preflights how much a prompt will cost against the window. ## All the gotchas (from re-verifying this end-to-end) Every one of these was hit and confirmed while testing on 2026-07-09: 1. **Install**: `npm install opencode-ai` can leave you with no working binary (post-install script skipped by npm `allow-scripts`). Use the `curl` installer; the binary lands at `~/.opencode/bin/opencode`. 2. **The 4096-token window is the whole story, and global `~/.claude/CLAUDE.md` is the usual killer.** opencode pastes `AGENTS.md`, local `CLAUDE.md`, and (undocumented Claude Code compatibility) global `~/.claude/CLAUDE.md` into the system prompt. A large global `CLAUDE.md` alone (~12 KB / ~3,300 tokens here) overflows the window before you type anything - HTTP 400. **Fix: `export OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1`** - proven to drop the request from 13,461 to 2,498 bytes (400 to 200). The `instructions` config field cannot remove it; only the env var does. 3. **Deny tools.** `"permission": { "*": "deny" }` stops opencode sending tool schemas, which otherwise consume a big slice of the 4096 tokens. 4. **Set a short agent `prompt`.** It replaces opencode's default agent instructions (verified: the custom prompt does take effect); without it the default coding-agent preamble is larger. 5. **`apiKey` must be present** in the provider `options` even though a local apfel server needs no auth - opencode's `@ai-sdk/openai-compatible` provider expects the field. Any placeholder works. 6. **Model id must match `/v1/models` exactly** (`apple-foundationmodel`). A mismatch fails the request. 7. **opencode makes two calls per turn**: a small title-generation call (always fits) plus the main agent call (the one that can overflow). Seeing the title call succeed but the answer fail is the classic 4096-overflow signature. 8. **`--pure` does not help the overflow** - it disables plugins, not instruction-file ingestion. 9. **Restart opencode after config changes** - it does not always hot-reload provider config. ## Verified session apfel 1.8.2 server, opencode 1.17.16, `lean` agent, with a 12 KB global `~/.claude/CLAUDE.md` present (the fix env var set): ``` $ OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1 opencode run --agent lean \ "In one sentence, what is a binary search?" > lean · apple-foundationmodel A binary search is an efficient algorithm for finding an item from a sorted array of items, by repeatedly dividing the search interval in half. ``` apfel's request log for that turn - every call `200 OK`, well inside the window, `$0.00`: ``` POST /v1/chat/completions 200 67ms stream tokens=~591 request bytes=2498 POST /v1/chat/completions 200 44ms stream tokens=~198 request bytes=713 ``` Without `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT=1`, the same turn sent 13,461 bytes and apfel returned `400 - Input exceeds the model's context window`. ## Credit The original config and the first working screenshot came from [@tvi (Tomas Virgl)](https://github.com/tvi). This page adds an end-to-end re-verification on current opencode and the 4096-token instruction-file gotcha. --- ### Plans/2026 06 21 Count Tokens Design # Design: `apfel --count-tokens` **Status:** Implemented — shipped in `feat/count-tokens` (PR #207) **Author:** Contributor design session (super-brainstorm) **Date:** 2026-06-21 ## Summary Add `apfel --count-tokens`, a zero-inference CLI mode that reports how many tokens a prompt would consume before calling the on-device model. This addresses the project's central constraint — the 4096-token context window — which users hit frequently when attaching files (`-f`) or MCP tool schemas (`--mcp`). ## Problem Users discover context overflow only at inference time (`[context overflow]`, exit 4) or via chat-only `--context-status`. Shell scripters, integration authors (opencode, Zed, custom agents), and MCP users need a pipe-friendly preflight that answers: **"Will this fit?"** ## Success Criteria - `apfel --count-tokens "prompt"` prints token count to stdout (plain or `-o json`) - Supports the same input resolution as normal prompt mode: positional prompt, stdin pipe, `-f` / `--system-file`, `-s` / `--system` - With `--mcp`, includes MCP tool-definition token cost in the breakdown (spawns MCP servers, same as inference) - Exit 0 by default (informational); `--strict` exits 4 when `total > budget` - Budget uses `--context-output-reserve` (default 512, same as inference via `ContextConfig.outputReserve`); `--max-tokens` does **not** affect preflight budget math - When Apple Intelligence / model is unavailable, continues with chars/4 fallback and `"approximate": true` in JSON (requires availability-gate exemption — see Architecture) - Documented in `docs/cli-reference.md`; cross-linked from `docs/tool-calling-guide.md` - TDD: unit tests in ApfelCore + CLIArgumentsTests; integration coverage in `cli_e2e_test.py` ## Out of Scope - Multi-turn chat history counting - HTTP server endpoint (`/v1/...`) - Embeddings, vision, or multi-model support - Changing inference or context-trimming behavior ## Architecture ### Components | Layer | Change | |-------|--------| | **ApfelCore** | New pure type `TokenBudgetReport` + aggregator for per-component sums and `fits` computation | | **CLI parsing** | New `Mode.countTokens` + `--count-tokens` / `--strict` flags in `CLIArguments.swift`; reject conflicts with `--serve`, `--chat`, `--stream`, `--benchmark`; extend file storage to retain `(path, content)` pairs for JSON breakdown | | **main.swift** | Add `.countTokens` to availability-gate exemption (alongside `.modelInfo`, `.serve`, `.update`) so preflight runs when model is unavailable; add `acceptsStdinInput: true` for countTokens mode | | **CLI execution** | New `countTokens()` in `CLI.swift`; reuses existing prompt/system/file resolution | | **MCP path** | When `--mcp` passed: init `MCPManager`, call `ContextManager.makeSession()` (same as `singlePrompt`), use returned `inputEntries` for accurate tool-schema counting | | **No-MCP path** | Build entries via `makeSession` + `makePromptEntry`; count with `TokenCounter.shared.count(entries:)` | | **Output** | Plain: human-readable summary; `-o json`: structured breakdown | ### Key Files - `Sources/Core/TokenBudgetReport.swift` (new, pure ApfelCore) - `Sources/CLI/CLIArguments.swift` (add `Mode.countTokens`, `fileAttachments: [(path: String, content: String)]`, `--strict`) - `Sources/CLI.swift` - `Sources/main.swift` - `Tests/apfelTests/TokenBudgetTests.swift` (new) - `Tests/apfelTests/CLIArgumentsTests.swift` - `Tests/integration/cli_e2e_test.py` - `docs/cli-reference.md` - `docs/tool-calling-guide.md` ### Reused Infrastructure - `TokenCounter.shared.count(entries:)` — real token counts (SDK 26.4+) with chars/4 fallback - `TokenCounter.shared.inputBudget(reservedForOutput:)` — budget math - `sessionInputEntries()` / `ContextManager.makeSession()` — accurate MCP tool schema counting (#176) - Existing CLI input resolution and `-o json` output patterns ## Data Flow 1. Parse args → validate no conflicting modes 2. Resolve prompt, system prompt, and file attachments (existing helpers) 3. Optionally init MCP and discover tools 4. Build `inputEntries` (ContextManager path if MCP, simple path otherwise) 5. Count total and per-component tokens 6. Compute `budget = contextSize - contextConfig.outputReserve` (from `--context-output-reserve`, default 512) 7. Emit output; exit 0 (or 4 with `--strict` if `total > budget`) ## Token Accounting **Authoritative total:** `total = TokenCounter.count(entries: inputEntries)` on the fully assembled entries that would be sent to inference — same assembly path as `singlePrompt`. **Components are non-overlapping slices** (for breakdown only; they must not double-count): | Field | What it counts | |-------|----------------| | `prompt_tokens` | Positional prompt + stdin content (stdin has no path; rolls into prompt, not `file_tokens`) | | `system_tokens` | System instructions text only (`-s` / `--system-file`) | | `file_tokens[]` | Each `-f` / `--system-file` attachment individually, keyed by retained path | | `mcp_tool_tokens` | Delta: `count(inputEntries with MCP) - count(inputEntries without MCP tools)` using identical prompt/system/files | Component sums may not exactly equal `total` due to assembly join separators (`\n\n` between files and prompt in `main.swift`); **`total` is always authoritative** for `fits` / `--strict`. ## Output Format ### JSON (`-o json`) ```json { "prompt_tokens": 42, "system_tokens": 128, "file_tokens": [{"path": "README.md", "tokens": 890}], "mcp_tool_tokens": 340, "total": 1400, "budget": 3584, "output_reserve": 512, "fits": true, "approximate": false, "context_size": 4096 } ``` Note: `total` reflects full assembled `inputEntries`; component fields are informational slices. ### Plain One-line summary to stdout. Optional per-component breakdown on stderr when not `--quiet`. ## Error Handling | Condition | Behavior | |-----------|----------| | Invalid flag combination | Exit 2 (existing CLI parse error pattern) | | MCP spawn / discovery failure | Exit 1 with clear error message | | Model unavailable | Continue with chars/4 fallback; `approximate: true`; note on stderr | | `--strict` and over budget | Exit 4 | ## Testing Strategy (TDD) 1. **Red:** `TokenBudgetTests` — aggregation math, budget with/without `--context-output-reserve`, `fits` logic, component-vs-total non-overlap (pure, no Apple Intelligence) 2. **Red:** `CLIArgumentsTests` — `--count-tokens` / `--strict` happy path + conflicts with `--serve`/`--chat`/`--stream`; file path retention 3. **Green:** Implement `TokenBudgetReport` + `countTokens()` wiring 4. **Integration:** `cli_e2e_test.py` model-free validation of flag presence and JSON shape 5. **Local (Apple Intelligence Mac):** real count with `-f README.md` and `--mcp mcp/calculator/server.py` 6. **Gate:** `swift run apfel-tests` (CI) + `make test` (full local qualification) ## Backwards Compatibility Additive CLI flag only. No API breakage. No changes to HTTP server or existing flag behavior. ## Risks - **Fallback accuracy:** chars/4 on unavailable model is approximate — mitigated by `approximate` field and stderr note - **MCP startup cost:** counting with `--mcp` spawns servers — acceptable for preflight; document in cli-reference - **Per-file breakdown:** requires counting files individually in addition to combined total — small extra TokenCounter calls ## Documentation - `docs/cli-reference.md` — new flag section with examples - `docs/tool-calling-guide.md` — "preflight your budget" cross-link - README Quick Start (UNIX section) — one example: `apfel --count-tokens -f README.md "summarize"` ## Decision Log | Decision | Choice | Rationale | |----------|--------|-----------| | Exit code default | Informational (exit 0) | Pipe-friendly, composable UNIX tool | | Strict mode | Opt-in `--strict` → exit 4 | CI/scripts opt in explicitly | | Output reserve | `--context-output-reserve` (default 512) | Matches inference (`ContextManager` uses `contextConfig.outputReserve`, not `--max-tokens`) | | `--max-tokens` in preflight | Does not affect budget | max-tokens caps generation length, not input budget reservation | | Model unavailable | Graceful fallback + gate exemption | Useful in CI/docs; `.countTokens` exempt from availability precheck in `main.swift` | | Token total | Authoritative `inputEntries` count | Components are informational slices; avoid double-counting merged file content | | Contribution workflow | GitHub issue first | Standard OSS etiquette for new features | ## GitHub Issue Draft **Title:** `feat(cli): apfel --count-tokens for token budget preflight` **Body:** > Preflight token counting for shell scripters and MCP users hitting the 4096-token wall. > > - `--count-tokens` with same inputs as prompt mode (stdin, `-f`, `-s`, `--mcp`) > - `-o json` breakdown; `--strict` exits 4 when over budget > - Budget: `contextSize - --context-output-reserve` (default 512), matching inference > - Runs when model unavailable (chars/4 fallback, `approximate: true`) > > Full spec: `docs/plans/2026-06-21-count-tokens-design.md` --- ### Plans/2026 06 21 Count Tokens Worklog # Work log: apfel --count-tokens **Branch:** feat/count-tokens **Design spec:** docs/plans/2026-06-21-count-tokens-design.md **Status:** ready-for-pr ## Progress checklist - [x] Wave 0: branch + baseline tests - [x] Wave 1: TokenBudgetReport (ApfelCore) - [x] Wave 2: CLI parsing - [x] Wave 3: execution wiring - [x] Wave 4: user-facing docs - [x] Wave 5: integration tests - [ ] Verification + PR (unit tests green; push/PR pending fork) ## Session log (append newest first) ### 2026-06-22 — Implementation complete **Done:** - `TokenBudgetReport` + `TokenBudgetTests` (ApfelCore) - `Mode.countTokens`, `--strict`, `fileAttachments` in CLIArguments - `countTokens()` in CLI.swift; main.swift dispatch + availability exemption - `TokenCounter` fast chars/4 path when model unavailable - Approximate path skips `LanguageModelSession` when AI unavailable - Docs: cli-reference, tool-calling-guide, README, man/apfel.1.in - Integration tests: help, JSON shape, strict exit **Tests run:** - `swift run apfel-tests` → pass (687 tests) - `swift build -c release` → pass - `make generate-man-page` → pass - Release smoke test on Mac with Apple Intelligence: first `tokenCount` call can be slow (model load) **Decisions / deviations from spec:** - `TokenCounter.count` / `count(entries:)` return chars/4 immediately when `!isAvailable` (avoids hang without AI) - Approximate mode totals merged prompt + system only (MCP delta skipped when unavailable) **Blockers / next up:** - Fork upstream and open PR ## Files touched (running list) | File | Status | Notes | |------|--------|-------| | Sources/Core/TokenBudgetReport.swift | done | new | | Tests/apfelTests/TokenBudgetTests.swift | done | new | | Sources/CLI/CLIArguments.swift | done | Mode, strict, fileAttachments | | Sources/CLI.swift | done | countTokens + help | | Sources/main.swift | done | dispatch, pipedContent | | Sources/Models.swift | done | TokenBudgetJSONResponse | | Sources/TokenCounter.swift | done | unavailable fast path | | Tests/apfelTests/CLIArgumentsTests.swift | done | | | Tests/apfelTests/main.swift | done | | | Tests/integration/cli_e2e_test.py | done | 3 tests | | docs/cli-reference.md | done | | | docs/tool-calling-guide.md | done | | | README.md | done | one example | | man/apfel.1.in | done | | | docs/plans/* | done | spec + worklog | ## PR readiness - [x] All waves complete - [x] Unit tests green (687) - [ ] Integration tests (pytest not installed locally; CI will run model-free subset) - [x] Docs updated - [x] Work log reflects final state --- ### Superpowers/Specs/2026 04 15 Apfel Language Guides Design # Design - apfel language guides **Date:** 2026-04-15 **Status:** Approved (brainstorming phase) **Next step:** writing-plans skill -> implementation plan ## Goal Ship a set of SEO-optimized, empirically tested "how to use the Apple Foundation Model from ``" guides in the apfel repo, backed by a separate lab repo that holds the runnable scripts and pytest harness that prove every code block actually works against a live `apfel --serve`. ## Scope **In scope - 10 scripting-language guides (v1):** Tier 1 (highest search volume): 1. Python 2. PHP 3. Ruby 4. Node.js (JavaScript/TypeScript) 5. Bash / curl Tier 2 (Mac-native / Mac-shipped scripting): 6. AppleScript 7. Swift scripting (`swift-sh` / shebang) 8. Zsh (distinct from Bash - default macOS shell) 9. Perl (ships with macOS) 10. AWK (ships with macOS) **Out of scope (YAGNI):** - Tier 3 niche scripting (Lua, Tcl, R, Elixir, Groovy, Raku) - Compiled languages (Go, Rust, Java, C#, Kotlin) - Framework-specific guides (Django, Rails, Laravel, Next.js) - Docker / devcontainer setups - Video or interactive playground - Translations (English only v1) ## Two repositories ### `Arthur-Ficial/apfel` (this repo) - docs only New directory: `docs/guides/` ``` docs/guides/ ├── index.md # Hub landing page, SEO-tuned ├── python.md ├── php.md ├── ruby.md ├── nodejs.md ├── bash-curl.md ├── applescript.md ├── swift-scripting.md ├── zsh.md ├── perl.md └── awk.md ``` Linked from: - `README.md` (new "Using from other languages" section) - `docs/integrations.md` (cross-link) ### `Arthur-Ficial/apfel-guides-lab` (new repo) - runnable proof ``` apfel-guides-lab/ ├── README.md # "This is the lab; apfel docs are in the main repo" ├── Makefile # make test, make capture, make test- ├── conftest.py # pytest: boots `apfel --serve` on :11434, waits /health ├── pyproject.toml # pytest + tooling ├── scripts/ │ ├── python/ │ │ ├── 01_oneshot.py │ │ ├── 02_stream.py │ │ ├── 03_json.py │ │ ├── 04_errors.py │ │ ├── 05_tools.py │ │ └── 06_example.py │ ├── php/ # same 6 files, .php │ ├── ruby/ # same 6 files, .rb │ ├── nodejs/ # same 6 files, .mjs │ ├── bash-curl/ # same 6 files, .sh │ ├── applescript/ # same 6 files, .applescript (tools=N/A) │ ├── swift-scripting/ # same 6 files, .swift │ ├── zsh/ # same 6 files, .zsh │ ├── perl/ # same 6 files, .pl (tools=N/A) │ └── awk/ # same 6 files, .awk (tools=N/A, json best-effort) ├── tests/ │ ├── test_python.py │ ├── test_php.py │ ├── test_ruby.py │ ├── test_nodejs.py │ ├── test_bash_curl.py │ ├── test_applescript.py │ ├── test_swift_scripting.py │ ├── test_zsh.py │ ├── test_perl.py │ └── test_awk.py └── outputs/ # committed real stdout captures ├── python/ │ ├── 01_oneshot.txt │ └── ... └── / ``` ## Per-guide structure (identical skeleton, SEO-tuned) Every `docs/guides/.md` follows the same sections: 1. **H1**: `How to use the Apple Foundation Model from ` - exact-match SEO headline 2. **Intro** - what apfel is, why 100% on-device matters, what this guide covers (~3 sentences) 3. **Prerequisites** - macOS 26+, Apple Silicon, Apple Intelligence enabled, `brew install apfel`, `apfel --serve` running 4. **One-shot chat completion** - minimal working code + real captured output 5. **Streaming** - idiomatic streaming code + real captured output 6. **JSON mode / structured output** - `response_format: {"type": "json_object"}` + parsed result 7. **Error handling** - trigger a known 501 or timeout, show how to catch cleanly 8. **Tool calling** - where supported (Python, Node.js, PHP, Ruby via OpenAI SDKs). For AppleScript/AWK/Zsh/Bash/Perl/Swift: explicitly state "raw HTTP only - use Python or Node.js if you need tool-calling," then show a minimal raw JSON POST as proof of capability 9. **Real mini-example** - "summarize a file from stdin," idiomatic to the language 10. **Troubleshooting** - 3-5 common errors (server not running, Apple Intelligence disabled, wrong port, JSON parse errors) 11. **Tested with** - `apfel `, macOS , ` `, date. Link to the exact script in the lab repo at a pinned commit SHA ## Lab repo harness - **pytest** with `conftest.py` session-scoped fixture that: 1. Verifies `apfel` is on PATH (skip with clear message if not) 2. Boots `apfel --serve --port 11434` as a background subprocess 3. Polls `/health` until 200 (or fails with timeout) 4. Yields to tests 5. Sends `SIGTERM` on teardown; `SIGKILL` if it lingers - **One `test_.py` per language**, parametrized over the script files in that language's directory - **Each test** does `subprocess.run(script, capture_output=True, timeout=60)`: - asserts exit code 0 - asserts stdout is non-empty - asserts loose regex match (model output varies; no brittle exact-match assertions) - **`make capture`** re-runs every script against the live server and writes `outputs//