Loading repository data…
Loading repository data…
Arthur-Ficial / repository
The free AI already on your Mac. CLI tool, OpenAI-compatible server, and interactive chat — all on-device via Apple Intelligence. No API keys, no cloud, no downloads.
A transparent discovery signal based on current public GitHub metadata.
This score does not audit code, security, maintainers, documentation quality, or suitability. Verify the repository and its current documentation before adoption.
Apple Silicon Macs ship a built-in LLM via Apple FoundationModels. apfel exposes it as a UNIX tool and a local OpenAI-compatible server. 100% on-device. No API keys, no cloud.
| Mode | Command | What you get |
|---|---|---|
| UNIX tool | apfel "prompt" / echo "text" | apfel | Pipe-friendly answers, file attachments, JSON output, exit codes |
| OpenAI-compatible server | apfel --serve | Drop-in local http://localhost:11434/v1 backend for OpenAI SDKs |
apfel --chat - interactive REPL.
Tool calling works in all contexts. 4096-token context.
macOS 26 Tahoe+, Apple Silicon (M1+), Apple Intelligence enabled.
brew install apfel
Update:
brew upgrade apfel
Build from source (Command Line Tools with macOS 26.4 SDK / Swift 6.3, no Xcode):
git clone https://github.com/Arthur-Ficial/apfel.git && cd apfel && make install
Nix, same-day tap, Mint, mise, troubleshooting: docs/install.md.
Quote prompts with ! in single quotes (zsh/bash history expansion): apfel 'Hello, Mac!'.
# Single prompt
apfel "What is the capital of Austria?"
# Permissive mode - reduces guardrail false positives for creative/long prompts
apfel --permissive "Write a dramatic opening for a thriller novel"
# Stream output
apfel --stream "Write a haiku about code"
# Pipe input
echo "Summarize: $(cat README.md)" | apfel
# Attach file content to prompt
apfel -f README.md "Summarize this project"
# Attach multiple files
apfel -f old.swift -f new.swift "What changed between these two files?"
# Attach a PDF or image - on-device text extraction, OCR, and "what the image is about"
apfel -f report.pdf "Summarize the key findings"
apfel -f receipt.jpg "What is the total?"
# Pipe a file straight in (PDF, image, or text)
cat report.pdf | apfel "Summarize this"
# Combine files with piped input
git diff HEAD~1 | apfel -f CONVENTIONS.md "Review this diff against our conventions"
# Only the code - no prose, no markdown fences (pipe-safe, exit 7 if empty)
apfel --code "a python function that deduplicates a list" > dedupe.py
apfel --code "shell one-liner to find the 10 largest files here" | pbcopy
# JSON output for scripting
apfel -o json "Translate to German: hello" | jq .content
# Guaranteed schema-valid JSON output (guided generation)
apfel --schema person.schema.json "Extract the person: Alice is 30." | jq .name
# One-shot multi-turn: conversation JSON in, next assistant turn out
jq '. += [{"role":"user","content":"and in German?"}]' conv.json | apfel --messages -
# Preflight token budget before a large prompt
apfel --count-tokens -f README.md "Summarize this"
# System prompt
apfel -s "You are a pirate" "What is recursion?"
# System prompt from file
apfel --system-file persona.txt "Explain TCP/IP"
# Quiet mode for shell scripts
result=$(apfel -q "Capital of France? One word.")
apfel --serve # foreground
brew services start apfel # background (like Ollama)
brew services stop apfel
APFEL_TOKEN=$(uuidgen) APFEL_MCP=/path/to/tools.py brew services start apfel
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"apple-foundationmodel","messages":[{"role":"user","content":"Hello"}]}'
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="unused")
resp = client.chat.completions.create(
model="apple-foundationmodel",
messages=[{"role": "user", "content": "What is 1+1?"}],
)
print(resp.choices[0].message.content)
Background service details: docs/background-service.md.
apfel --chat is a small REPL for testing prompts or MCP servers. For a GUI chat app, see apfel-chat.
apfel --chat
apfel --chat -s "You are a helpful coding assistant"
apfel --chat --mcp ./mcp/calculator/server.py # chat with MCP tools
apfel --chat --debug # debug output to stderr
Ctrl-C exits. Context is trimmed automatically (docs/context-strategies.md).
Real shell scripts that wrap apfel: cmd (English to shell command), oneliner, mac-narrator, wtd, explain, naming, port, gitsum. They are bundled in the binary - no repo clone. Write them out wherever you installed apfel from (homebrew-core, the tap, or source):
apfel demos ./apfel-demos
That writes every demo (executable) plus a README.md into ./apfel-demos. Then run them from there:
./apfel-demos/cmd "find all .log files modified today"
# $ find . -name "*.log" -type f -mtime -1
./apfel-demos/cmd -x "show disk usage sorted by size" # -x = execute after confirm
./apfel-demos/cmd -c "list open ports" # -c = copy to clipboard
Re-run apfel demos after brew upgrade apfel to refresh. Sources: demo/. Walkthroughs and a copy-paste cmd shell function for your .zshrc: docs/demos.md.
Attach Model Context Protocol servers with --mcp. apfel discovers, invokes, and returns.
apfel --mcp ./mcp/calculator/server.py "What is 15 times 27?"
mcp: ./mcp/calculator/server.py - add, subtract, multiply, divide, sqrt, power, round_number ← stderr
tool: multiply({"a": 15, "b": 27}) = 405 ← stderr
15 times 27 is 405. ← stdout
Use -q to suppress tool info.
apfel --mcp ./server_a.py --mcp ./server_b.py "Use both tools"
apfel --serve --mcp ./mcp/calculator/server.py
apfel --chat --mcp ./mcp/calculator/server.py
Ships with a calculator at mcp/calculator/ (docs/mcp-calculator.md).
Remote MCP servers (Streamable HTTP, MCP spec 2025-03-26):
apfel --mcp https://mcp.example.com/v1 "what tools do you have?"
# bearer token - prefer env var (flag is visible in ps aux)
APFEL_MCP_TOKEN=mytoken apfel --mcp https://mcp.example.com/v1 "..."
# mixed local + remote
apfel --mcp /path/to/local.py --mcp https://remote.example.com/v1 "..."
Security: prefer
APFEL_MCP_TOKENover--mcp-token(ps aux). apfel refuses bearer tokens over plaintexthttp://.
apfel itself has no config file - flags + env vars, like any UNIX tool. If you want a TOML config (many MCPs, profiles, team configs in git), apfel-run is an MIT wrapper that adds one via execve drop-in.
brew install Arthur-Ficial/tap/apfel-run
apfel-run config init # starter ~/.config/apfel/config.toml
alias apfel=apfel-run # optional, every apfel flag still works
Base URL: http://localhost:11434/v1
| Feature | Status | Notes |
|---|---|---|
POST /v1/chat/completions | Supported | Streaming + non-streaming |
POST /v1/responses | Supported | OpenAI Responses API: string/message input, instructions, streaming (canonical event sequence), text.format (incl. json_schema), function tools (non-streaming). Stateful features (previous_response_id, store: true, background, reasoning, hosted tools) return honest 501s |
GET /v1/models | Supported | Returns apple-foundationmodel |
GET /health | Supported | Model availability, context window, languages |
GET /v1/logs, /v1/logs/stats | Debug only | Requires --debug |
| Tool calling | Supported | Native ToolDefinition + JSON detection. See docs/tool-calling-guide.md |
response_format: json_object | Supported | System-prompt injection; markdown fences stripped from output |
response_format: json_schema | Supported | Guaranteed schema-conforming output via FoundationModels DynamicGenerationSchema; works with stream: true |
temperature, top_p, max_tokens, seed | Supported | Mapped to GenerationOptions. top_p is nucleus sampling; temperature: 0 maps to greedy (deterministic). Omitting max_tokens uses the remaining context window (drop-in OpenAI semantics) - see Default response cap |
stream: true | Supported | SSE; final usage chunk only when stream_options: {"include_usage": true} (per OpenAI spec) |
finish_reason | Supported | stop, tool_calls, length |
| Context strategies | Supported | x_context_strategy, x_context_max_turns, x_context_output_reserve extension fields |
| CORS | Supported | Enable with --cors |
POST /v1/completions | 501 | Legacy text completions not supported |
POST /v1/embeddings | 501 | Embeddings not available on-device |
logprobs=true, n>1, stop, presence_penalty, frequency_penalty | 400 | Rejected explicitly. n=1 and logprobs=false are accepted as no-ops |
| Multi-modal (images) | 400 | Rejected with clear error |
Authorization header | Supported | Required when --token is set. See docs/server-security.md |
Full API spec: openai/openai-openapi.
max_tokens)When max_tokens is omitted, CLI and OpenAI-compatible server behave identically: the value flows through as nil and the model uses whatever room is left in the 4096-token context window. This is drop-in OpenAI semantics - no arbitrary fallback constant.
The on-device model has a 4096-token context window that holds input and output combined. If generation runs into the ceiling, the response ends cleanly with finish_reason: "length" and the partial content is returned (server: HTTP 200; CLI: exit 0 with a stderr warning). Pass max_tokens explicitly when you want a tighter latency budget or a known cap for your client.
# Omitted: uses remaining window, finish_reason: "stop" or "length"
curl -sS http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"apple-foundationmodel",
"messages":[{"role":"user","content":"Reply SKIP, MOVE, or RENAME."}]}'
# Explicit cap (recommended for tight latency budgets)
curl -sS http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"apple-foundationmodel","max_tokens":128,
"messages":[{"role":"user","content":"Summarise: ..."}]}'
| Use case | max_tokens |
|---|