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
Google 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_fraction of 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_fraction of the last scan
  • on MySQL, a table, column or trigger that vanished from information_schema but 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.