Feature detection — supports(model, capability) over the atlas registry (4 providers)
Status: DOCUMENTED (design against the model / tool record schemas in CLAUDE.md and the alias conventions of the four models fragments) · offline-tested with tests/shared/fixtures/{models,tools}.json (11 model records across openai/anthropic/xai/gemini; tests/shared/test_supports.py, 12 tests; supports.ts --selftest)
Sources: CLAUDE.md record schemas; generated/fragments/models/openai-models.json (snapshots, canonical_model, record_kind), anthropic-models.json (aliases), xai-models.json (kind: model|alias|retired_redirect|legacy, aliases with parenthetical notes, redirect targets in verification.request_note — e.g. grok-3 → grok-4.3 confirmed live 2026-09-19), gemini-models.json (kind: stable|preview|alias|experimental|agent, -latest records with dict aliases {alias, resolves_to_live, history} — gemini-flash-latest → gemini-3.8-flash live 2026-09-19, dict-shaped tools[] {type, category, support}); generated/fragments/tools/{xai,gemini}-tools.json
Last verified: 2026-09-19
Implementation: examples/shared/feature-detection/supports.py (stdlib) and supports.ts.
Why
Hard-coding "model X supports tools" in application code rots within weeks — faster now that four vendors ship monthly. The atlas already answers the question in generated/models.json (capabilities{} with true | false | "unknown", tools[], aliases[], snapshots[]) and generated/tools.json (compatible_models[]). The registry turns those files into three-valued predicates that applications, the comparator and the provider-choosing agent call at runtime, and papers over the four vendors' different alias conventions and capability vocabularies.
Semantics
| Query | Returns | Rule |
|---|---|---|
supports(model, cap) |
true / false / "unknown" |
Precedence: (1) the exact key in capabilities — a literal boolean yields true/false, an explicit "unknown" wins over everything below; (2) a synonym key from CAPABILITY_SYNONYMS with a boolean value; (3) tool inference: tool_<x> → supports_tool(model, x) (full tri-state); a bare name only counts when it positively matches a tool type (web_search on grok-4.3's tools list → true), never a silent false for unknown words. Missing key, non-boolean metadata (lists, notes), unknown model or no capabilities object → "unknown". |
supports_tool(model, tool) |
tri-state | tool is a name (web_search, googleSearch) or a versioned type (web_search_20250305). Match = exact, date-suffix-stripped, or name20…/namev… prefix after normalisation (lower-case, punctuation removed → google_search ≡ googleSearch). Gemini dict entries use their support value (true/false/"Supported (Preview)" → true/"Not supported" → false). tools.json compatible_models is a positive-only signal. Non-empty tools[] without a match → false; empty/missing → "unknown". |
tools_for(model) |
list[str] |
Union of the model's tools[] types (dict entries with support: false dropped) and tool records whose compatible_models include it, de-duplicated by normalised key (urlContext vs url_context). |
models_with(*caps, provider=, include_unknown=False) |
list[str] |
Canonical ids where all capabilities are true (or true/"unknown" with include_unknown). Skips record_kind: "snapshot", `kind: alias |
compare(a, b) |
{cap: (tri, tri)} |
Raw provider keys (no synonym folding) — feeds the comparator, which maps synonyms explicitly. |
canonical_id(x) |
str | None |
Resolution below. |
providers() |
list[str] |
Distinct providers present. |
Alias, snapshot and redirect resolution
- Case-insensitive;
openai/,anthropic/,xai/,gemini/prefixes tolerated; Gemini resource prefixesmodels/andtunedModels/stripped (models/gemini-3.8-flash). aliases[]andsnapshots[]strings map to their record; trailing parenthetical notes are stripped ("grok-voice-latest (routes here since 2026-08-05)"→grok-voice-latest,"grok-4-fast (probed)"→grok-4-fast).- Dict aliases
{alias, resolves_to_live, evidence, history}on Gemini-latestrecords (kind: "alias"): the record id itself resolves toresolves_to_livewhen that id exists (gemini-flash-latest→gemini-3.8-flash;gemini-pro-latest→gemini-3.1-proper the 429quotaDimensions.model;gemini-flash-lite-lateststays a stub when the target is"unknown (not probed)"). - Explicit
resolves_to/redirects_to/redirect_to/alias_offields → target. - xAI
kind: "retired_redirect"→ target parsed fromverification.request_note("GET /v1/models/grok-3 -> 200 with id 'grok-4.3'"):grok-3,grok-4-0709,grok-4-fast-reasoning→grok-4.3. Records probeddocs_onlyhave no target and stay unresolved (queries →"unknown"). - A record with
canonical_model ≠ id(OpenAI snapshot-kind records) defers to the canonical record when its owncapabilitiesis empty; pointer records (kindalias/redirect, empty capabilities) are followed up to 8 hops with cycle protection. - Unlisted date suffixes (
-YYYYMMDDor-YYYY-MM-DD) are stripped as a last resort.
Cross-provider capability vocabulary
Provider agents emit their own keys. CAPABILITY_SYNONYMS (identical in py/ts) folds the common ones when the exact key is absent:
| Portable name | OpenAI keys | Anthropic keys (real fragment: tool_use, structured_outputs_json, …; tools[] are dicts {type, category, beta_header}) |
xAI keys | Gemini keys |
|---|---|---|---|---|
structured_outputs |
structured_outputs |
structured_outputs_json |
structured_outputs |
structured_output |
function_calling |
function_calling |
tool_use (+ parallel_tool_use, strict_tool_use) |
function_calling (+ parallel_tool_calls) |
function_calling (+ parallel_function_calling, tool function_declarations) |
web_search |
tool_web_search / tools web_search |
web_search + tools web_search_20250305 |
web_search (explicit "unknown" on grok-4.3: docs list server tools only on the grok-4.6 page) + tools web_search |
google_search_grounding + tool google_search |
prompt_caching |
prompt_caching |
prompt_caching (+ prompt_caching_1h_ttl) |
prompt_caching_automatic |
context_caching_implicit, context_caching_explicit |
reasoning / extended_thinking |
reasoning |
thinking_extended_manual_budget, thinking_adaptive, interleaved_thinking |
reasoning, reasoning_effort_parameter |
thinking (per-model thinking{} object, not a boolean) |
code_execution |
tools code_interpreter |
code_execution + tools code_execution_* |
code_execution (unknown) + tools code_interpreter, code_execution |
code_execution + tool code_execution |
mcp |
tool_mcp / tools mcp |
mcp_connector + tool mcp_toolset |
remote_mcp (unknown) + tool mcp |
mcp_servers (UNVERIFIED) |
computer_use |
tool_computer_use / tools computer_use_preview |
computer_use + tools computer_toolset_* |
— | computer_use + tool computer_use ("Supported (Preview)") |
Real-data spot check (2026-09-19, Registry.load() on the rebuilt generated/): models_with("web_search", "structured_outputs", "prompt_caching") returns models from all four providers (OpenAI gpt-4.1/gpt-5.x, Anthropic Claude 4.x, xAI grok-4.6, Gemini 2.5/3.x); canonical_id("grok-3") == "grok-4.3", canonical_id("models/gemini-flash-latest") == "gemini-3.8-flash"; gemini-pro-latest stays unresolved because its live target (gemini-3.1-pro, from the 429 quotaDimensions) is not a record id.
Explicit keys always win: supports("grok-4.3", "web_search") is "unknown" (the flag says so) even though supports_tool("grok-4.3", "web_search") is true (the type string is accepted by the API) — surface both to the user.
Accepted file shapes
A JSON array; {"records": [...]}, {"models": [...]}, {"tools": [...]}; or an object keyed by id. Missing or malformed files load as empty registries (every query → "unknown" / []).
Usage
from supports import Registry
reg = Registry.load() # generated/models.json + generated/tools.json
reg.providers() # ['anthropic', 'gemini', 'openai', 'xai']
reg.canonical_id("grok-3") # 'grok-4.3' (retired redirect)
reg.canonical_id("models/gemini-flash-latest") # 'gemini-3.8-flash'
reg.supports("gemini-3.8-flash", "structured_outputs") # True (← structured_output)
reg.supports("grok-4.3", "web_search") # 'unknown' — explicit flag; reg.supports_tool(...) → True
reg.models_with("web_search", "structured_outputs", "prompt_caching") # cross-provider, synonyms folded
reg.models_with("web_search", "structured_outputs", "prompt_caching", include_unknown=True, provider="xai")import { Registry } from "./supports.ts";
const reg = Registry.load();
reg.supportsTool("gemini-3.8-flash", "googleSearch"); // true (== "google_search")
reg.modelsWith(["function_calling"], { provider: "gemini" });CLI: python3 examples/shared/feature-detection/supports.py <model> <capability>.
Consumers
- Provider adapters (
docs/architecture/multi-provider-abstraction.md): refuse or warn before sendingjson_schema/ tools to a model that returnsfalse; ask for confirmation on"unknown"; pick the Gemini-latesttarget explicitly when pinning matters (the alias moved three times in 2026). - Fallback chains (
resilience.md): pick fallback models withmodels_with(...), per provider, so the fallback actually supports the request. - Capability graph exporter (
capability-graph.md): same tri-state (SUPPORTS/SUPPORTS_UNKNOWN/ no edge) plusALIAS_OFandREDIRECTS_TOedges built from the same alias rules — but no synonym folding in the graph (query both keys).
Limitations
- The synonym table is small and hand-maintained; keys outside it (e.g. Gemini
live_api, xAIdeferred_completions) are provider-specific by nature. - "Supported" ≠ "supported on every endpoint": xAI server tools are Responses-only (Chat Completions → 422), Gemini tools differ between generateContent, Interactions and Live — consult
endpoints[]andtools.json.compatible_endpoints. - Account restrictions are not capabilities:
gemini-3.1-pro-previewsupports everything on paper and returns 429limit: 0on a free-tier key; xAIgrok-embedding-smallisACCOUNT_RESTRICTED. Checkstatus[](ACCOUNT_RESTRICTED) alongsidesupports().