Reference
Everything RadarModel 0.2 does, in one page: radarmodel-ai first, then radarmodel-schema. The README on GitHub says the same in the repository.
Commands
radarmodel-ai init [-config FILE] write a starter config
radarmodel-ai check [-config FILE] [flags] scan once, compare, report
radarmodel-ai watch [-config FILE] [flags] scan every interval until stopped
radarmodel-ai show [-config FILE] SOURCE list the stored models of a source
radarmodel-ai version
The config is radarmodel.json in the current folder, or the file in $RADARMODEL_CONFIG, or -config FILE.
Flags for check and watch:
| Flag | What it does |
|---|---|
-min info|warning|breaking |
Lowest severity shown at the terminal. Warning by default; the rest is counted |
-json |
Print the report as JSON |
-no-alerts |
Skip webhook, Slack and email |
-dry-run |
check only: compare, but keep the stored snapshots |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Scanned, nothing breaking |
| 1 | At least one breaking change |
| 2 | A provider couldn’t be scanned, another run holds the state folder, or the config is wrong |
Breaking wins: a run with a breaking change and a failed provider exits 1, and the report shows both.
Configuration
{
"interval": "1h",
"timeout": "30s",
"min_fraction": 0.5,
"confirm_removals": true,
"state_dir": ".radarmodel",
"providers": [
{"type": "openai"},
{"type": "anthropic"},
{"type": "google"},
{"type": "openrouter"},
{"type": "openai", "name": "proxy", "base_url": "https://llm-proxy.example.com", "api_key_env": "PROXY_KEY"}
],
"watch": ["openai:gpt-4o", "anthropic:claude-sonnet-4-5", "google:gemini-2.5-flash", "openrouter:anthropic/*", "proxy:gpt-4o"],
"alerts": {
"min_severity": "warning",
"webhook": {"url_env": "RADAR_WEBHOOK_URL"},
"slack": {"url_env": "SLACK_WEBHOOK_URL"},
"email": {"host": "smtp.example.com", "port": 587, "from": "[email protected]",
"to": ["[email protected]"], "username_env": "SMTP_USER", "password_env": "SMTP_PASS"}
}
}
| Setting | Default | What it does |
|---|---|---|
providers |
none | The lists to read. type is openai, anthropic, google or openrouter |
providers[].name |
the type | Name used in reports, watch entries and file names. Two entries of one type need different names |
providers[].base_url |
the provider’s API | A proxy or compatible endpoint. file:// reads saved answers from disk (Linux and macOS) |
providers[].api_key_env |
OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY |
Variable holding the key. OpenRouter needs none |
watch |
none | Models your app calls, as source:model. * matches anything, slashes included; a source of * means every provider |
interval |
1h |
Time between scans in watch mode. At least 1m |
timeout |
30s |
Limit for one HTTP request |
min_fraction |
0.5 |
A list with fewer than this share of the last one’s models is a failed scan. 0 turns it off |
confirm_removals |
true |
A watched removal is breaking only when a second scan agrees. false reports it on the first |
state_dir |
.radarmodel |
Where snapshots are kept, relative to the config file |
alerts.min_severity |
warning |
Lowest severity that sends an alert. A failed scan always sends one |
alerts.webhook |
none | url_env, or url for a URL with no token. Gets the JSON report |
alerts.slack |
none | url_env naming the incoming-webhook URL. Gets a text summary |
alerts.email |
none | host, port (587), from, to, and username_env and password_env if the server needs them. STARTTLS when offered |
Unknown fields are errors, and so are watch entries naming a source that isn’t configured, source names that differ only in case or punctuation, and secret variables that are named but not set.
What counts as what
Only watched models can be breaking or warnings. Every other change is info.
| Change to a watched model | Severity |
|---|---|
| Gone from the list, on two scans in a row | breaking |
| Gone from the list on one scan | warning |
| Context or output limit went down | breaking |
Lifecycle became retired (Anthropic) |
breaking |
Lifecycle left active, such as deprecated (Anthropic) |
warning |
| Got or changed a retirement date (Anthropic, OpenRouter) | warning |
| A limit is no longer published | warning |
| Price went up, or a price appeared or vanished | warning |
| Lost a supported method (Google) | warning |
Named in watch but not in the list |
warning, every run |
| New models, price cuts, bigger limits, name changes | info |
When a watched model disappears and a model with the same display name appears in the same scan, the report adds “possibly renamed to …”.
What each provider shows
| Provider | What’s compared |
|---|---|
| OpenAI | Model IDs and owner |
| Anthropic | Display name, input and output limits, lifecycle, retirement date |
| Display name, version, input and output limits, supported methods | |
| OpenRouter | Name, context length, max output, input and output price per token, retirement date |
What a failed scan is
A failed scan reports an error for that provider, changes nothing in its stored snapshot, and lets the other providers scan as usual. These are all failed scans:
- an HTTP error. 408, 429 and 5xx get three attempts in all, honouring Retry-After up to 30 seconds
- a body that isn’t valid JSON, is cut off, or has no model list
- an empty list
- a page of a paged list failing, or a paged answer where one page was expected
- fewer models than
min_fractionof the last list - a redirect to another host, refused so keys can’t follow it
- a timeout
Files
state_dir holds latest/SOURCE.json, the snapshot the next scan compares with, and the last 50 snapshots in history/SOURCE/. Each file is written whole or not at all. A lock file keeps two runs from using the folder at once; one older than 30 minutes is taken to be left from a crash.
RadarModel Schema
radarmodel-schema watches Postgres, MySQL, MariaDB and SQLite schemas. It shares the exit codes, the -min, -json, -no-alerts and -dry-run flags, the alerts block and the state folder with radarmodel-ai.
Schema commands
radarmodel-schema init [-config FILE] write a starter config
radarmodel-schema check [-config FILE] [flags] [DB...] scan, compare, report
radarmodel-schema watch [-config FILE] [flags] scan every interval until stopped
radarmodel-schema accept [-config FILE] DB... take the current schema as the baseline
radarmodel-schema diff [-config FILE] DB1 DB2 compare two databases now
radarmodel-schema show [-config FILE] DB list the stored objects of a database
radarmodel-schema version
The config is radarmodel-schema.json in the current folder, or the file in $RADARMODEL_SCHEMA_CONFIG, or -config FILE. diff exits 0 when the two databases match and 1 when they don’t. When each side uses a single schema under a different name, such as app_staging and app, the names are lined up first.
Schema configuration
{
"interval": "1h",
"timeout": "2m",
"min_fraction": 0.5,
"ignore": ["*.tmp_*"],
"databases": [
{"name": "prod", "driver": "postgres", "dsn_env": "PROD_DATABASE_URL", "schemas": ["public", "billing"]},
{"name": "app", "driver": "mysql", "dsn_env": "APP_MYSQL_DSN", "baseline": "pinned"},
{"name": "local", "driver": "sqlite", "path": "app.db", "ignore": ["main.sessions"]}
],
"alerts": {"min_severity": "warning", "slack": {"url_env": "SLACK_WEBHOOK_URL"}}
}
| Setting | Default | What it does |
|---|---|---|
databases[].driver |
none | postgres, mysql (MySQL or MariaDB) or sqlite |
databases[].dsn_env |
none | Variable holding the connection string: a URL or keyword string for Postgres, user:pass@tcp(host:3306)/dbname for MySQL. It never appears in output or alerts |
databases[].path |
none | SQLite file, relative to the config file. Opened read-only |
databases[].schemas |
all but system schemas (Postgres), the connection’s database (MySQL) | Schemas to watch |
databases[].baseline |
rolling |
rolling compares with the last scan; pinned with the last accepted schema |
ignore, databases[].ignore |
none | Globs on qualified names, such as *.tmp_* or main.sessions. A pattern that matches a table covers what’s in it |
min_fraction |
0.5 |
From 10 tables and views up, finding fewer than this share of them is a failed scan. 0 turns it off |
timeout |
2m |
Limit for a whole scan of one database |
interval, state_dir, alerts |
as for radarmodel-ai |
Adding an ignore rule never shows up as objects being dropped: the rules apply to the stored snapshot too.
What counts as what in a schema
| Change | Severity |
|---|---|
| A table, view or column dropped | breaking |
| A column’s type changed | breaking |
| A column became NOT NULL | breaking |
| A new table, view, column, index, constraint or trigger | warning |
| An index, constraint or trigger dropped or redefined | warning |
| A column’s default, generated expression or collation changed, or it became nullable | warning |
| A view’s definition changed | warning |
| A table’s storage engine or kind changed, a partition attached or detached, row-level security switched | warning |
| A column’s position changed | info |
| The server’s version changed | info |
| Anything inside a table that was itself added or dropped | info |
What each database shows
| Database | What’s compared |
|---|---|
| Postgres 12 to 17 | Tables, views and their definitions, partitions, row-level security; columns with type, NOT NULL, default, generated or identity, collation, position; full index, constraint and trigger definitions, and whether a trigger is enabled. Objects that belong to an extension are left out |
| MySQL 8.0 and 8.4, MariaDB 10.6 and later | Tables with engine, views; columns with type, NULL, default, extra and generated expression, collation, position; indexes with method, columns, order, expressions and visibility; primary keys, unique keys, foreign keys with their rules, checks and whether they’re enforced; triggers |
| SQLite | Tables with their CREATE statement, views; columns with type, NOT NULL, default, generated, position; indexes and triggers by their CREATE statement; foreign keys |
SQLite foreign keys have no names, so they’re named after their columns and the table they reference, such as main.orders.fk_customer_id_customers. MySQL integer display widths (int(11)) are left out, so an upgrade past 8.0.19, which stopped printing them, isn’t a type change. When the server’s version changes, the report says so and notes it on the other changes of that scan.
When a schema scan fails
A failed scan reports an error for that database, changes nothing in its stored snapshot, and lets the other databases scan as usual. These are all failed scans:
- a lost connection, an unreachable server, a wrong password, a connection string that’s not set or can’t be parsed
- a scan longer than
timeout - a SQLite file that’s missing, isn’t a database or is a folder
- a schema that kept changing while it was read. Every scan reads twice and only accepts two identical reads, with three tries
- fewer tables than
min_fractionof the last scan - on MySQL, a table, column or trigger that vanished from
information_schemabut answers “access denied” instead of “doesn’t exist”
On Postgres, the catalog lists every object whatever the user’s privileges, so a revoked GRANT changes nothing. Each read sets its own search_path, IntervalStyle and DateStyle, so a role or connection string with other settings doesn’t change how definitions read.
Database privileges
On Postgres any role that can connect is enough. On MySQL and MariaDB:
GRANT SELECT, SHOW VIEW, TRIGGER ON app.* TO 'radar'@'%';
information_schema only lists what the user can access. With SELECT, a dropped table and a hidden one answer differently, so the first scan checks that every table can be read and fails if one can’t. TRIGGER is what lets MySQL list the triggers. If a privilege was taken away on purpose and the objects really are gone for good, radarmodel-schema accept DB takes the schema as it is now.