Anonymous product analytics
Rapid-MLX sends anonymous product analytics by default. You see a
notice before any event can be sent, and you can turn analytics off at
any time — rapid-mlx telemetry off — and keep using every
Rapid-MLX feature.
How the notice works. This is a notice, not an opt-in
prompt. Interactive CLI installs see the disclosure once per revision;
Desktop shows a one-time in-app acknowledgement notice. A long-running
headless server prints a one-line disclosure on every start while
analytics are enabled. Other headless commands print it once per
revision. A Desktop sidecar declared with
RAPID_MLX_PROCESS_ROLE=desktop-sidecar stays silent until
the app has shown the current notice. A refusal recorded by a release
older than 0.15.0 is switched to on only after that notice is shown,
and that run sends nothing. A refusal recorded by 0.15.0 or later is
never reversed automatically.
Why we collect it
We use named, metadata-only product events to understand which models,
APIs, and integrations people use; where setup or serving fails; and
whether installs keep using Rapid-MLX. For engine v2, the event and
property allowlist below is the contract: an unknown event, property,
type, or enum value is dropped instead of sent. The native Desktop app's
separate events are disclosed below.
Engine v2’s data boundary
- Prompts, model outputs, generated text, attachments, or other content.
- File paths, hostnames, usernames, API keys, flag values, exception messages, or raw User-Agent strings.
- Country, location, or coordinates. GeoIP enrichment is disabled for every event.
- Your IP address as an event property. Like any HTTPS service, PostHog receives the connection IP, but the project is configured to discard it.
- Private model names or caller-supplied model names.
Model identity follows one narrow rule. A model in Rapid-MLX’s public
catalog is reported by its catalog alias. A non-catalog
org/name is reported only after a real request to the
canonical Hugging Face Hub succeeded anonymously, that proof is no more
than 30 days old, and no Hub token is or has been visible to the current
process. Authenticated, gated, missing, unproven, served-name, and other
private models become <custom>; local paths become
<local>.
Native Desktop events
The native Desktop app has a separate, older three-event stream that is
not part of the engine registry below. It sends session_start
once per launch with random install/session IDs and coarse app, OS, CPU,
chip, and memory metadata; activation once per install for a
first successful chat, dictation, or image milestone; and
error for a crash or unhandled exception. An error event can
include the error type and message, a context label, and the top 30 stack
frames with file/line information but no captured locals. Usernames and
temporary-container identifiers in diagnostic paths are redacted.
These native events do not include prompts, responses, attachments,
transcripts, generated images, traffic content, or tool API keys. See the
Desktop privacy policy
for the complete native-app contract.
Where events go
Official engine release builds send events directly from your
machine to https://us.i.posthog.com/batch/ in
PostHog Cloud’s US region. Rapid-MLX sets
$geoip_disable: true and
$process_person_profile: false on every event. The PostHog
project also has “Discard client IP data” enabled. No person profile is
built.
Only official release builds may transmit: the engine requires both a
release-workflow stamp and embedded PostHog key, and rejects source
checkouts, editable installs, forks, locally rebuilt packages, and CI.
Events are capped at 30 per event name per minute and 1,000 per process
session; overflow is dropped.
The native Desktop events described above instead go to
https://telemetry.rapidmlx.com/v1/events. That Cloudflare
Worker derives a coarse two-letter country code from connection metadata
(XX when unavailable), strips the client IP, and stores the
event in R2; the IP address is never stored. The app and embedded engine
share one consent record and anonymous install ID.
Turn analytics off
The persistent command-line controls are:
$ rapid-mlx telemetry off # alias: disable
$ rapid-mlx telemetry on # alias: enable
$ rapid-mlx telemetry status
$ rapid-mlx telemetry preview
$ rapid-mlx telemetry reset-id
$ rapid-mlx telemetry reset
reset-id rotates only the install ID and keeps the preference.
reset is a distinct action: it deletes the stored preference,
rotates an existing install ID, clears identity-keyed activation markers,
and leaves the permanent consent lock file in place. The next run is
treated as a new install; reset itself emits no event.
rapid-mlx telemetry off attempts to send one final
telemetry_opted_out event before saving the off preference.
The consent gate permits that event only after the current notice has
already been delivered and while transmission is otherwise allowed;
after the preference is saved, later events are blocked.
These per-process controls take precedence over the stored setting:
$ RAPID_MLX_TELEMETRY=0 rapid-mlx serve
$ DO_NOT_TRACK=1 rapid-mlx serve
$ rapid-mlx --no-telemetry serve
In the Desktop app, turn off “Send anonymous usage data” in
Settings → Privacy. The CLI and Desktop share the same machine-wide
preference. The capture path checks the current decision again while the
process is running, with a live-state cache of no more than five seconds.
Run rapid-mlx telemetry status to see the effective state and
the rule that produced it. Run rapid-mlx telemetry preview to
print the exact v2 event that would be sent, without transmitting it.
Separate version-check poll
The CLI’s background version check is separate from telemetry. It polls
https://rapidmlx.com/api/cli-update with the installed version
and normal HTTPS connection metadata; it sends no install ID or usage
event. Disable this poll with
RAPID_MLX_DISABLE_VERSION_CHECK=1.
During first run, Desktop can also send
POST https://rapidmlx.com/api/desktop-funnel with exactly the
app version and one allowlisted milestone name: onboarding shown, model
download started/completed/failed, engine ready/start failed, or first
chat reply. The request has no install ID, device, OS, chip, RAM, model
name, error text, client timestamp, account, or country field. The server
stores only aggregate counts by milestone, UTC day, and app version, plus
the last accepted time; it stores no identifier, per-install path, or
country. Only installs whose first-run setup starts on a Desktop version
that includes this feature send these counters; existing installs never
do. Each milestone is sent at most once per install because a local marker
file prevents repeats, and internal or development builds do not send.
The connecting IP is used transiently for rate limiting; the IP
address itself is never stored, and the minute-scoped rate-limit state is
deleted after two minutes. Desktop skips this endpoint after
telemetry has been declined or when update checks are turned off.
Retention and legal basis
Engine v2 events remain in the US PostHog project under that project’s
retention policy; the engine client defines no separate deletion period
and keeps no second raw-event copy. Native Desktop raw events in R2 age
out on a rolling 30-day window. Analytics use is based on Rapid-MLX’s
legitimate interest in improving the product under GDPR Article 6(1)(f).
You have the right to object: after the CLI’s final opt-out attempt and
preference write, the off setting blocks later events. The environment
kill switches block transmission immediately.
Anonymous install identity
Events carry a random install UUID so activity can be linked across
sessions, plus a fresh UUID for each process session. They do not carry
an account identity. PostHog receives anonymous events without creating
person profiles.
Verify it yourself: the event registry is a
public file in the engine repository,
and this page is generated from it.
Complete event reference
This section is generated from the engine’s strict public registry. Unknown events, properties, types, or enum values are rejected. The source is registry version 1, vendored from engine commit e55622264914a9d27b34860c01ad9a056dcbd692.
Common properties on every event
Design sec 1.2 — stamped on EVERY event, validated by validate_common() before the transport initialises (fail closed). Dropped vs. 0.14.3: country (GeoIP is off), the dead 'engine' slot, the constant 'status'. The two pattern-capped kinds ('version', 'uuid') exist only here; event properties may not use them.
| Property | Presence | Kind | Allowed values / shape | Registry note |
app_version | required | pattern-capped string | Maximum 32 characters; pattern ^[0-9]{1,3}\.[0-9]{1,4}\.[0-9]{1,4}(rc[0-9]{1,3}|\.dev[0-9]{1,6})?$. | — |
surface | required | enum (surface) | cli, server, desktop | — |
os | required | enum (os) | darwin, linux, windows, other | — |
os_version | required | pattern-capped string | Maximum 10 characters; pattern ^[0-9]{1,4}\.[0-9]{1,4}$. | major.minor only — redact.platform_info() already truncates it. |
arch | required | enum (arch) | arm64, x86_64, other | — |
chip | required | enum (chip) | m1, m1-pro, m1-max, m1-ultra, m2, m2-pro, m2-max, m2-ultra, m3, m3-pro, m3-max, m3-ultra, m4, m4-pro, m4-max, m4-ultra, m5, m5-pro, m5-max, m5-ultra, m6, m6-pro, m6-max, m6-ultra, apple-other, intel, other | — |
memory_gb | required | integer | 0 through 4096 | redact.bucket_memory_gb() — rounded to whole GB, never exact bytes. |
python_version | optional | pattern-capped string | Maximum 8 characters; pattern ^[0-9]{1,2}\.[0-9]{1,3}$. | Engine surfaces only; the desktop app omits it. |
install_id | required | UUID | A UUID string. | — |
session_id | required | UUID | A UUID string. | — |
channel | required | enum (channel) | stable, rc | — |
nth_model_served | optional | integer | 0 through 10000 | Cohort stamp: distinct models this install has EVER served, at emit time. OPTIONAL: omitted when the local state store cannot answer (read-only HOME, locked or corrupt database — store.days_since_first_run_bucket() returns None and store.note_model_served() returns 0 by design there). A wire value of 0 is valid ONLY when a store genuinely answered zero: note_model_served() returns the count INCLUDING the model just noted (success is >= 1) and 0 only on FAILURE (an unusable id or a storage failure), so an emitter MUST map a returned 0 to None and omit the key. Analysts treat ABSENCE as 'unknown', never as 0 / first day. |
days_since_first_run_bucket | optional | enum (days_since_first_run_bucket) | 0, 1, 2-6, 7-29, 30+ | OPTIONAL: omitted when the local state store cannot answer (read-only HOME, locked or corrupt database). Analysts must treat ABSENCE as 'unknown', never as 0 / first day. |
app_opened
Launches. Common props carry everything that matters. CLI and server launches add the closed top-level command name; Desktop omits it.
| Property | Presence | Kind | Allowed values / shape | Registry note |
command | optional | enum (cli_command) | system-one, cua, serve, bench, benchmark, models, recipe, ls, version, help, pull, import, rm, alias, ps, upgrade, update, chat, run, info, agents, start, connect, doctor, telemetry, feedback, share, launch, service, bare, other | — |
server_start_state
Whether an accepted server invocation reached a listening socket.
| Property | Presence | Kind | Allowed values / shape | Registry note |
state | required | enum (server_start_state) | attempted, ready, failed | — |
model_type | optional | enum (model_type) | llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other | — |
load_policy | optional | enum (load_policy) | eager, lazy, none | — |
previous_run_unterminated | optional — only when state ∈ {attempted} | boolean | true or false | — |
port_explicit | optional — only when state ∈ {failed} and failure_stage ∈ {bind} | boolean | true or false | — |
failure_stage | optional — only when state ∈ {failed} | enum (failure_stage) | resolve, download, preflight, prepare, engine_start, bind | — |
active_day
Once per install per UTC day, on the first successful inference — DAU and retention that still work for a server that runs for weeks. The design table writes 'surface' against this row; surface is already a common prop on every event, so repeating it here would put the same key on the payload twice. It is not duplicated.
| Property | Presence | Kind | Allowed values / shape | Registry note |
| No event-specific properties. |
model_pulled
What people want to run, and whether the mirror served it. preflight / via_suggestion: the bring-your-own-model funnel (enum byom_preflight); via_suggestion is sent only as true, when a preflight refusal on this install suggested this model in the last seven days. Matching happens on the device against one-way digests; no suggested name is ever sent.
| Property | Presence | Kind | Allowed values / shape | Registry note |
model | required | model id | Catalog alias or proven-public org/name; reserved: <custom>, <local>. Maximum 128 characters; pattern ^(<custom>|<local>|[A-Za-z0-9._-]{1,96}(/[A-Za-z0-9._-]{1,96})?)$. | — |
model_type | required | enum (model_type) | llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other | — |
source | required | enum (pull_source) | mirror, hf | — |
size_bucket | optional | enum (size_bucket) | lt_1gb, 1_2gb, 2_4gb, 4_8gb, 8_16gb, 16_32gb, 32_64gb, 64gb_plus, unknown | — |
preflight | optional | enum (byom_preflight) | passed, refused, no_verdict, skipped, cached | — |
via_suggestion | optional | boolean | true or false | — |
model_pull_failed
_failed twin: same identifying props, all optional, plus error_class. For a preflight refusal (preflight=refused) only, suggestion and support_request record the closed alternative kind and the closed outcome of the opt-in support-request offer.
| Property | Presence | Kind | Allowed values / shape | Registry note |
model | optional | model id | Catalog alias or proven-public org/name; reserved: <custom>, <local>. Maximum 128 characters; pattern ^(<custom>|<local>|[A-Za-z0-9._-]{1,96}(/[A-Za-z0-9._-]{1,96})?)$. | — |
model_type | optional | enum (model_type) | llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other | — |
source | optional | enum (pull_source) | mirror, hf | — |
size_bucket | optional | enum (size_bucket) | lt_1gb, 1_2gb, 2_4gb, 4_8gb, 8_16gb, 16_32gb, 32_64gb, 64gb_plus, unknown | — |
error_class | required | enum (pull_error_class) | network, gated, disk_full, not_found, unsupported_format, unsupported_architecture, other | — |
preflight | optional | enum (byom_preflight) | passed, refused, no_verdict, skipped, cached | — |
via_suggestion | optional | boolean | true or false | — |
suggestion | optional — only when preflight ∈ {refused} | enum (byom_suggestion) | mlx_build, catalog, none | — |
support_request | optional — only when preflight ∈ {refused} | enum (support_request_outcome) | not_eligible, non_interactive, declined, no_answer, sent, busy, unreachable | — |
model_served
What people actually run. preflight / via_suggestion: as on model_pulled.
| Property | Presence | Kind | Allowed values / shape | Registry note |
model | required | model id | Catalog alias or proven-public org/name; reserved: <custom>, <local>. Maximum 128 characters; pattern ^(<custom>|<local>|[A-Za-z0-9._-]{1,96}(/[A-Za-z0-9._-]{1,96})?)$. | — |
model_type | required | enum (model_type) | llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other | — |
auto_selected | required | boolean | true or false | — |
quant | required | enum (quant) | 2bit, 3bit, 4bit, 6bit, 8bit, bf16, fp16, mxfp4, nvfp4, dwq, other, unknown | — |
preflight | optional | enum (byom_preflight) | passed, refused, no_verdict, skipped, cached | — |
via_suggestion | optional | boolean | true or false | — |
model_serve_failed
What people want and CANNOT have — the highest-value row in the table. The design table also lists 'architecture (allow-list, else other)'. It is NOT here: the architecture string comes straight out of a checkpoint's config.json (see cli.py:6122) and the engine holds no allow-list to close it against, so shipping it would mean either a free-form string (forbidden by sec 1.5) or an invented enum. It lands when a real allow-list exists; error_class already separates unsupported_architecture from the other failure modes. For missing_extra only, extra_recovery records the closed recovery decision after prompting and before any installer runs. failure_stage names the closed startup boundary the failure crossed (same enum as server_start_state); it is emitted only when the caller states it explicitly — telemetry never guesses a stage. preflight / via_suggestion / suggestion / support_request: the bring-your-own-model funnel, exactly as on model_pull_failed.
| Property | Presence | Kind | Allowed values / shape | Registry note |
model | optional | model id | Catalog alias or proven-public org/name; reserved: <custom>, <local>. Maximum 128 characters; pattern ^(<custom>|<local>|[A-Za-z0-9._-]{1,96}(/[A-Za-z0-9._-]{1,96})?)$. | — |
model_type | optional | enum (model_type) | llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other | — |
auto_selected | optional | boolean | true or false | — |
quant | optional | enum (quant) | 2bit, 3bit, 4bit, 6bit, 8bit, bf16, fp16, mxfp4, nvfp4, dwq, other, unknown | — |
error_class | required | enum (serve_error_class) | unsupported_architecture, unsupported_format, insufficient_memory, corrupt_weights, download_failed, local_path_missing, missing_extra, invalid_config, tokenizer_load_failed, incompatible_weights, quantization_mismatch, invalid_model_ref, backend_load_failed, other | — |
failure_stage | optional | enum (failure_stage) | resolve, download, preflight, prepare, engine_start, bind | — |
extra | optional — only when error_class ∈ {missing_extra} | enum (optional_extra) | vision, video, audio, image | — |
extra_recovery | optional — only when error_class ∈ {missing_extra} | enum (extra_recovery) | accepted, declined, no_answer, interrupted, non_interactive, assume_yes, no_installer, managed_runtime, broken_runtime | — |
preflight | optional | enum (byom_preflight) | passed, refused, no_verdict, skipped, cached | — |
via_suggestion | optional | boolean | true or false | — |
suggestion | optional — only when preflight ∈ {refused} | enum (byom_suggestion) | mlx_build, catalog, none | — |
support_request | optional — only when preflight ∈ {refused} | enum (support_request_outcome) | not_eligible, non_interactive, declined, no_answer, sent, busy, unreachable | — |
capability_rejected
Features people reach for that we do not have. reject_reason is present only for structured_output_unsupported and context_length_exceeded, and contains no free-form text.
| Property | Presence | Kind | Allowed values / shape | Registry note |
capability | required | enum (capability) | image_input_unsupported, video_input_unsupported, audio_input_unsupported, embeddings_unavailable, image_generation_unavailable, video_generation_unavailable, multi_sample_unsupported, logprobs_unsupported, logit_bias_unsupported, structured_output_unsupported, tool_type_unsupported, stateless_api_only, fim_suffix_unsupported, context_length_exceeded, speculative_decoding_unsupported, mcp_unsupported, speech_capability_unsupported, runtime_extra_missing, response_format_unsupported, perf_overrides_unsupported, other | — |
model_type | required | enum (model_type) | llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other | — |
model | optional | model id | Catalog alias or proven-public org/name; reserved: <custom>, <local>. Maximum 128 characters; pattern ^(<custom>|<local>|[A-Za-z0-9._-]{1,96}(/[A-Za-z0-9._-]{1,96})?)$. | — |
caller | optional | enum (caller) | claude-code, cursor, aider, cline, continue, codex, opencode, pi, qwen-code, deepseek-harness, openai-python, openai-node, anthropic-sdk, litellm, langchain, llamaindex, ollama, python-httpx, python-requests, python-aiohttp, node-fetch, axios, curl, okhttp, go-http, rapid-agents, rapid-bench, rapid-cli-chat, rapid-desktop, rapid-gradio, unknown, other | — |
reject_reason | optional — only when capability ∈ {structured_output_unsupported, context_length_exceeded} | enum (reject_reason) | format_type_unsupported, prompt_over_window, operational_cap, other | — |
inference_bucket_reached
Which models / APIs / clients carry real load, and how many installs hit repeated failures. One event per local counter threshold crossing: O(log n) events, no timing trail, no sampling. Read it as unique installs that reached bucket >= N — never as a sum. A failure carries error_class (closed enum inference_error_class, never error text); failure counters are kept per class, so a failed bucket is 'installs that hit >= N failures of THIS class'.
| Property | Presence | Kind | Allowed values / shape | Registry note |
model | required | model id | Catalog alias or proven-public org/name; reserved: <custom>, <local>. Maximum 128 characters; pattern ^(<custom>|<local>|[A-Za-z0-9._-]{1,96}(/[A-Za-z0-9._-]{1,96})?)$. | — |
endpoint | required | enum (endpoint) | /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/audio/transcriptions, /v1/messages, /v1/images/generations, /v1/responses, other | — |
caller | required | enum (caller) | claude-code, cursor, aider, cline, continue, codex, opencode, pi, qwen-code, deepseek-harness, openai-python, openai-node, anthropic-sdk, litellm, langchain, llamaindex, ollama, python-httpx, python-requests, python-aiohttp, node-fetch, axios, curl, okhttp, go-http, rapid-agents, rapid-bench, rapid-cli-chat, rapid-desktop, rapid-gradio, unknown, other | — |
result | required | enum (result) | ok, failed | — |
count_bucket | required | enum (count_bucket) | 1, 2, 3_4, 5_9, 10_19, 20_49, 50_99, 100_199, 200_499, 500_999, 1000_plus | — |
bucket_source | required | enum (bucket_source) | crossed_now, observed_existing | — |
error_class | optional — only when result ∈ {failed} | enum (inference_error_class) | insufficient_memory, engine_aborted, template_error, media_input_invalid, prompt_too_large, strict_schema_violation, model_replaced, output_contract_unmet, stream_error, other | — |
model_imported
`rapid-mlx import` published a converted, smoke-tested model. Not sent when an identical import already existed (a no-op re-run). model is the SOURCE's telemetry_model_id (catalog alias, proven-public org/name, '<local>' or '<custom>'), never the import's own name; quant is the requested width.
| Property | Presence | Kind | Allowed values / shape | Registry note |
model | required | model id | Catalog alias or proven-public org/name; reserved: <custom>, <local>. Maximum 128 characters; pattern ^(<custom>|<local>|[A-Za-z0-9._-]{1,96}(/[A-Za-z0-9._-]{1,96})?)$. | — |
quant | required | enum (quant) | 2bit, 3bit, 4bit, 6bit, 8bit, bf16, fp16, mxfp4, nvfp4, dwq, other, unknown | — |
model_import_failed
_failed twin: same identifying props, all optional, plus error_class.
| Property | Presence | Kind | Allowed values / shape | Registry note |
model | optional | model id | Catalog alias or proven-public org/name; reserved: <custom>, <local>. Maximum 128 characters; pattern ^(<custom>|<local>|[A-Za-z0-9._-]{1,96}(/[A-Za-z0-9._-]{1,96})?)$. | — |
quant | optional | enum (quant) | 2bit, 3bit, 4bit, 6bit, 8bit, bf16, fp16, mxfp4, nvfp4, dwq, other, unknown | — |
error_class | required | enum (import_error_class) | invalid_ref, metadata_unavailable, unsupported_format, already_quantized, unsupported_architecture, name_conflict, insufficient_disk, insufficient_memory, download_failed, convert_failed, smoke_failed, interrupted, other | — |
agent_configured
Which integrations matter. Emitted only after setup changes configuration and any requested server verification succeeds.
| Property | Presence | Kind | Allowed values / shape | Registry note |
agent | required | enum (agent) | aider, claude-code, codex, continue, deepseek-harness, hermes, kilo-code, langchain, opencode, openhands, pi, pydanticai, qwen-code, smolagents, other | — |
agent_configure_failed
_failed twin: same identifying props, all optional, plus error_class.
| Property | Presence | Kind | Allowed values / shape | Registry note |
agent | optional | enum (agent) | aider, claude-code, codex, continue, deepseek-harness, hermes, kilo-code, langchain, opencode, openhands, pi, pydanticai, qwen-code, smolagents, other | — |
error_class | required | enum (agent_error_class) | no_safe_setup_flow, config_invalid, config_changed, config_write_failed, server_not_ready, server_no_models, model_not_advertised, other | — |
telemetry_opted_out
The cost of default-on, measured. Sent once, then silence.
| Property | Presence | Kind | Allowed values / shape | Registry note |
via | required | enum (opt_via) | cli, settings | — |
telemetry_opted_in
The other half of the same measurement.
| Property | Presence | Kind | Allowed values / shape | Registry note |
via | required | enum (opt_via) | cli, settings | — |
Releases before 0.15.0
Releases before 0.15.0 use the previous opt-in system. Telemetry is off until a
person accepts the first-run prompt or runs
rapid-mlx telemetry enable. If enabled, those clients send
the older session, sampled request, error, and activation schema to the
Rapid-MLX Cloudflare collector—not the v2 event list above. Installs still
on 0.14.x that had opted in keep sending that old schema to the old
collector until they upgrade. Their
rapid-mlx telemetry disable,
RAPID_MLX_TELEMETRY=0, DO_NOT_TRACK=1, and
--no-telemetry controls continue to work.
Talk to us in the Rapid-MLX Discord
or open a GitHub issue.
The complete registry above is published so changes are reviewable before
a release.