Reference · telemetry v2 · View source

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

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.

PropertyPresenceKindAllowed values / shapeRegistry note
app_versionrequiredpattern-capped stringMaximum 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})?$.—
surfacerequiredenum (surface)cli, server, desktop—
osrequiredenum (os)darwin, linux, windows, other—
os_versionrequiredpattern-capped stringMaximum 10 characters; pattern ^[0-9]{1,4}\.[0-9]{1,4}$.major.minor only — redact.platform_info() already truncates it.
archrequiredenum (arch)arm64, x86_64, other—
chiprequiredenum (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_gbrequiredinteger0 through 4096redact.bucket_memory_gb() — rounded to whole GB, never exact bytes.
python_versionoptionalpattern-capped stringMaximum 8 characters; pattern ^[0-9]{1,2}\.[0-9]{1,3}$.Engine surfaces only; the desktop app omits it.
install_idrequiredUUIDA UUID string.—
session_idrequiredUUIDA UUID string.—
channelrequiredenum (channel)stable, rc—
nth_model_servedoptionalinteger0 through 10000Cohort 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_bucketoptionalenum (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.

PropertyPresenceKindAllowed values / shapeRegistry note
commandoptionalenum (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.

PropertyPresenceKindAllowed values / shapeRegistry note
staterequiredenum (server_start_state)attempted, ready, failed—
model_typeoptionalenum (model_type)llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other—
load_policyoptionalenum (load_policy)eager, lazy, none—
previous_run_unterminatedoptional — only when state ∈ {attempted}booleantrue or false—
port_explicitoptional — only when state ∈ {failed} and failure_stage ∈ {bind}booleantrue or false—
failure_stageoptional — 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.

PropertyPresenceKindAllowed values / shapeRegistry 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.

PropertyPresenceKindAllowed values / shapeRegistry note
modelrequiredmodel idCatalog 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_typerequiredenum (model_type)llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other—
sourcerequiredenum (pull_source)mirror, hf—
size_bucketoptionalenum (size_bucket)lt_1gb, 1_2gb, 2_4gb, 4_8gb, 8_16gb, 16_32gb, 32_64gb, 64gb_plus, unknown—
preflightoptionalenum (byom_preflight)passed, refused, no_verdict, skipped, cached—
via_suggestionoptionalbooleantrue 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.

PropertyPresenceKindAllowed values / shapeRegistry note
modeloptionalmodel idCatalog 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_typeoptionalenum (model_type)llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other—
sourceoptionalenum (pull_source)mirror, hf—
size_bucketoptionalenum (size_bucket)lt_1gb, 1_2gb, 2_4gb, 4_8gb, 8_16gb, 16_32gb, 32_64gb, 64gb_plus, unknown—
error_classrequiredenum (pull_error_class)network, gated, disk_full, not_found, unsupported_format, unsupported_architecture, other—
preflightoptionalenum (byom_preflight)passed, refused, no_verdict, skipped, cached—
via_suggestionoptionalbooleantrue or false—
suggestionoptional — only when preflight ∈ {refused}enum (byom_suggestion)mlx_build, catalog, none—
support_requestoptional — 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.

PropertyPresenceKindAllowed values / shapeRegistry note
modelrequiredmodel idCatalog 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_typerequiredenum (model_type)llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other—
auto_selectedrequiredbooleantrue or false—
quantrequiredenum (quant)2bit, 3bit, 4bit, 6bit, 8bit, bf16, fp16, mxfp4, nvfp4, dwq, other, unknown—
preflightoptionalenum (byom_preflight)passed, refused, no_verdict, skipped, cached—
via_suggestionoptionalbooleantrue 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.

PropertyPresenceKindAllowed values / shapeRegistry note
modeloptionalmodel idCatalog 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_typeoptionalenum (model_type)llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other—
auto_selectedoptionalbooleantrue or false—
quantoptionalenum (quant)2bit, 3bit, 4bit, 6bit, 8bit, bf16, fp16, mxfp4, nvfp4, dwq, other, unknown—
error_classrequiredenum (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_stageoptionalenum (failure_stage)resolve, download, preflight, prepare, engine_start, bind—
extraoptional — only when error_class ∈ {missing_extra}enum (optional_extra)vision, video, audio, image—
extra_recoveryoptional — only when error_class ∈ {missing_extra}enum (extra_recovery)accepted, declined, no_answer, interrupted, non_interactive, assume_yes, no_installer, managed_runtime, broken_runtime—
preflightoptionalenum (byom_preflight)passed, refused, no_verdict, skipped, cached—
via_suggestionoptionalbooleantrue or false—
suggestionoptional — only when preflight ∈ {refused}enum (byom_suggestion)mlx_build, catalog, none—
support_requestoptional — 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.

PropertyPresenceKindAllowed values / shapeRegistry note
capabilityrequiredenum (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_typerequiredenum (model_type)llm, vlm, embedding, image-gen, video-gen, text-diffusion, audio, other—
modeloptionalmodel idCatalog 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})?)$.—
calleroptionalenum (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_reasonoptional — 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'.

PropertyPresenceKindAllowed values / shapeRegistry note
modelrequiredmodel idCatalog 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})?)$.—
endpointrequiredenum (endpoint)/v1/chat/completions, /v1/completions, /v1/embeddings, /v1/audio/transcriptions, /v1/messages, /v1/images/generations, /v1/responses, other—
callerrequiredenum (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—
resultrequiredenum (result)ok, failed—
count_bucketrequiredenum (count_bucket)1, 2, 3_4, 5_9, 10_19, 20_49, 50_99, 100_199, 200_499, 500_999, 1000_plus—
bucket_sourcerequiredenum (bucket_source)crossed_now, observed_existing—
error_classoptional — 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.

PropertyPresenceKindAllowed values / shapeRegistry note
modelrequiredmodel idCatalog 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})?)$.—
quantrequiredenum (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.

PropertyPresenceKindAllowed values / shapeRegistry note
modeloptionalmodel idCatalog 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})?)$.—
quantoptionalenum (quant)2bit, 3bit, 4bit, 6bit, 8bit, bf16, fp16, mxfp4, nvfp4, dwq, other, unknown—
error_classrequiredenum (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.

PropertyPresenceKindAllowed values / shapeRegistry note
agentrequiredenum (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.

PropertyPresenceKindAllowed values / shapeRegistry note
agentoptionalenum (agent)aider, claude-code, codex, continue, deepseek-harness, hermes, kilo-code, langchain, opencode, openhands, pi, pydanticai, qwen-code, smolagents, other—
error_classrequiredenum (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.

PropertyPresenceKindAllowed values / shapeRegistry note
viarequiredenum (opt_via)cli, settings—

telemetry_opted_in

The other half of the same measurement.

PropertyPresenceKindAllowed values / shapeRegistry note
viarequiredenum (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.

Questions or objections

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.