Skip to main content

hank-model-authority

The model authority every product repository reads: which models exist (registry entries with their execution identity and per-family request facts), which model each capability uses (selection policy), and what each model costs (rate tables). The distribution depends on PyYAML and pydantic only, so Risk, Investment and Edgar take it without the gateway.

Mechanism — one artifact, admission, the host directory, lifecycle, skew — is docs/reference/model-authority.md; the data deploy is docs/runbooks/model-authority-deploy.md.

Layout

Module Owns
authority/model-authority.yaml, authority/rates/*.json the packaged artifact: one model-authority/2.x document (registry models, selection.capabilities, one revision) and per-provider prices
schema.py the version rule (SCHEMA_MAJOR/SCHEMA_MINOR), ModelAuthorityError and its message, protocol families and each family's compat: key set
loader.py HANK_MODEL_AUTHORITY_DIR resolution, the strict load, the directory digest
current.py the process's one import-time load: INITIAL_MODEL_REGISTRY, INITIAL_MODEL_SELECTION_POLICY, RATE_TABLES, HEALTH_FACT, health_fact()
registry.py, selection.py registry entries and selection policy
capabilities.py capability IDs and the process that executes each (a release, not data)
binding.py, bind.py the single resolver and CapabilityBind
rates.py, thinking.py rate tables; effort vocabulary
__main__.py python -m model_authority check <dir>: the ordinary load of a candidate directory

The package root imports types and the loader, never current, so python -m model_authority check <dir> can check a candidate directory while the host directory is broken.

Which directory a process loads

HANK_MODEL_AUTHORITY_DIR set: that directory, resolved once at start; missing or unreadable fails the start. Unset: the packaged authority/ directory. Every loading unit sets it — /etc/hank/model-authority in production, ~/.config/hank/model-authority on this Mac — so the packaged copy is what tests and unwired tools load. Every process reports what it admitted through health_fact(): {"revision", "schema", "sha256", "source", "loader", "admits"}; source is the resolved release path (…/model-authority.d/<revision>-<sha12>/…) or packaged:<path>.

Schema

This loader implements model-authority/2.3 and admits 2.0 through 2.3 artifacts (health_fact()["admits"] is model-authority/2.0-2.3): same major, minor at most its own, every other field and literal strict; each refusal is one ModelAuthorityError naming artifact, revision, schema, loader and admitted range.

Each entry's compat: key set is its protocol family's (schema.py): the profile maps to anthropic.messages, openai.chat_completions, openai.responses or google.generate_content (PROTOCOL_FAMILY_BY_PROFILE), and COMPAT_MODEL_BY_FAMILY holds each family's frozen key set. 2.1 adds two anthropic.messages keys, each derived when absent so 2.0 artifacts load unchanged, and each refused when it contradicts the 2.0 key it refines:

Key Values Absent Must agree with
thinking.off_value disabled, between_tools, null (cannot be turned off) disabled if can_disable, else null can_disable is true exactly when off_value is disabled
tool_choice_forcing any, thinking_off_only, never thinking_off_only if forced_tool_choice_requires_thinking_off, else any that flag is false exactly when forcing is any

The 2.0 keys stay required (a minor removes nothing), so an adapter that still reads can_disable or forced_tool_choice_requires_thinking_off keeps working; the gateway's Anthropic adapter reads both new keys. Sonnet 5.5 uses can_disable: false, off_value: between_tools: the legacy bool still means whether the literal thinking.type: disabled is legal.

Tier selection (2.2)

selection.tiers maps arbitrary non-empty role names to a default assignment and an optional allowed_model_keys ceiling. Assignments are one of:

  • {models: [key, ...], effort: high}: ordered, non-empty execution candidates; this tier must declare a ceiling containing every candidate.
  • {alias: deep, effort?: high}: use another tier's assignment, optionally replacing its effort. An absent ceiling inherits the target's ceiling over the base policy alias graph, before any override. Aliases must be acyclic and target an existing tier.
  • {kind: inherit_parent}: copy the existing exact parent bind; no role ceiling.

Capability and channel defaults may be {kind: tier, tier: default, effort?: high}. The static project_tier owner is shared by artifact admission and resolution: first candidate qualified for the capability, in both the slot and referring tier ceilings, and eligible for new selection. Effort is requested effort, then slot-local effort, then resolved tier effort (outer alias effort wins). The chosen candidate must support that exact effort; no clamp or next-candidate retry occurs. Existing entitlement and credential checks follow selection, and their failure never falls through to another candidate.

A reader that needs one capability's concrete no-override default (a startup default entry, a dry-run label, a contract check) calls project_capability_default(registry, policy, capability_id); it returns the kind: model default as written and projects kind: tier through project_tier. Never read .default.model_key directly: it is None for tiers.

resolve_capability_model adds keyword-only requested_role, role_map, role_map_source and requested_role_source. A map's entries replace tier defaults and may replace fallback chains; omitted roles use policy, and aliases resolve within that effective map. Redirecting an alias cannot widen the referring role's fixed base ceiling. Map values may be TierModelsDefault, TierAliasDefault, TierInheritParentDefault, or their literal mappings.

Existing explicit, saved and authenticated concrete-model inputs win first. Then requested role wins over channel/default tier selection. Selection source is, in order: inherited assignment → parent_binding; explicit-user requested role → explicit_user; an overridden assignment anywhere in its alias chain → role_map_source (saved_preference or internal_policy, required for a supplied map); other requested role → internal_policy; channel tier → channel_default; otherwise the existing capability-default source (capability_default for the driver, internal_policy for other slots). Role/map provenance does not extend concrete-choice permission flags or tools. CapabilityBind stays 1.0 with its existing source literals. Inherited role assignments still enforce both the requested role's fixed ceiling and the slot ceiling against the parent's model. Without a snapshot, concrete selections without their own effort use applicable base-policy tier effort, never map effort.

Fallback chains and presets (2.3)

A role policy may declare fallback alongside default and allowed_model_keys:

deep:
  default: {models: ["anthropic.claude-opus-5-5"], effort: high}
  allowed_model_keys: ["anthropic.claude-opus-5-5", "codex.gpt-6-1-sol", "anthropic.claude-sonnet-5-5"]
  fallback:
    - {model: "codex.gpt-6-1-sol"}
    - {model: "anthropic.claude-sonnet-5-5"}

Entries are ordered {model: key, effort?: exact} or {alias: role, effort?: exact}. An absent or empty list means no chain; an alias role with no list inherits its base target's chain, while its own list (including []) replaces inheritance. Entry aliases capture the target role's base-policy assignment, filtered by the target and containing role ceilings, never its fallback chain. Each alias is one hop: project_fallback_chain(registry, policy, capability_id, requested_role) chooses its first slot-qualified candidate inside the slot ceiling. The result contains ordered entries (FallbackModel) and omitted entries with typed reason codes; a slot unable to use an entry does not invalidate the artifact.

Admission checks entry keys, lifecycle, alias resolution, role ceilings and exact effort. An omitted entry effort is checked against the containing role's default effort at load but retained as None, meaning the bound effort at hop time. An explicit entry effort is never clamped. Fallback admission refusals retain a typed ModelAuthorityError.code (capability_model_unavailable, capability_model_not_allowed, capability_role_unknown, capability_role_unavailable, capability_effort_invalid, capability_effort_unsupported or capability_selection_invalid).

selection.presets maps preset names to partial role maps. Entries take the assignment shapes above plus optional fallback; ceilings remain base policy. For example, economy: {default: {models: ["anthropic.claude-sonnet-5-5", "anthropic.claude-sonnet-5-5-sdk"], effort: high}}. The admitted policy.presets and its maps are read-only. Unknown roles, alias cycles, unavailable keys, unsupported efforts and widened ceilings fail at load. Use role_map=policy.presets["economy"] with projection or snapshot capture. A preset entry that omits fallback keeps that role's policy chain; a supplied list replaces it. Fallback aliases remain base-policy assignments even when the preset changes their target's primary assignment.

Frozen selection snapshots

capture_selection_snapshot(selection_policy, role_map=..., role_map_source=...) returns an immutable SelectionSnapshot containing every capability's CapabilityDefaults (default plus by_channel, each a CapabilityDefault) and every capture-time tier's alias-resolved {models, effort} or {kind: inherit_parent} assignment, plus its resolved fallback_chains. overridden records roles whose effective alias chain used the winning map, even when that map value equals the policy value. source is the winning map's saved_preference or internal_policy literal exactly when overridden is non-empty; otherwise it is null. Independent captures compare by value.

snapshot.to_json() returns this closed, versioned form:

{"version": 1, "policy_revision": "revision", "assignments": {"default": {"models": ["model-key"], "effort": "high"}}, "fallback_chains": {"default": [{"models": ["fallback-key"], "effort": null}]}, "capabilities": {"session.driver": {"default": {"kind": "tier", "tier": "default"}, "by_channel": {}}}, "overridden": ["default"], "source": "saved_preference"}

The example shows one slot; a snapshot holds every capability that existed at capture. Top-level keys have the order shown; assignments, fallback chains, capabilities, channels and overridden names are sorted, while candidate arrays and chain entries preserve order. SelectionSnapshot.from_json round-trips the value; missing/unknown keys, wrong types, unresolved aliases, unknown efforts and empty candidate arrays raise TierProjectionError with capability_selection_invalid. Older snapshot JSON without fallback_chains loads with empty chains for all captured roles. Each frozen chain entry holds the resolved base candidates for one hop and its optional exact effort. project_fallback_chain accepts the same role_map or selection_snapshot inputs; frozen chains use current slot/role ceilings and qualification, not a later policy's chain.

Pass selection_snapshot=snapshot to resolve_capability_model or project_tier instead of role_map (supplying both raises ValueError). Slot defaults, channel defaults, slot-local effort and tier assignments are frozen; a later slot repoint cannot select a newly published tier or concrete model. The referring tier's ceiling, slot allowance, permission flags, capability exposure, lifecycle, entitlement and credentials remain current. A tier absent from the snapshot or removed from current policy is capability_role_unknown; no currently eligible candidate is capability_role_unavailable. A snapshot is validated on its own contents, not against today's capability set, so one journaled before a release that adds or retires a capability still loads and serves its captured slots; a requested slot it did not capture is capability_policy_missing, and a frozen model key the current registry no longer has is capability_model_unavailable. The exact effort rules and source precedence above still apply: only overridden roles use snapshot.source, while the others retain today's selection-source literal. Concrete explicit, saved or run-override selections without effort use the frozen applicable slot default's effort when a snapshot is supplied. Omitting the snapshot (or passing None) leaves resolution unchanged.

Artifacts declaring 2.0 or 2.1 cannot contain tiers or kind: tier; artifacts older than 2.3 cannot contain fallback or selection.presets. The packaged artifact intentionally stays 2.1 until consumer loaders upgrade; publishing new minor data before those upgrades would prevent those consumers from starting.

Changing it

  • A data change (a model, a default, a price, a compat value) edits authority/ and ships by scripts/deploy_model_authority.sh, not by a release (runbook). Declare the lowest schema minor that covers the fields the artifact uses.
  • A schema change is a release: a new optional field or literal is a minor (SCHEMA_MINOR), a removal or redefinition a major. Deploy loaders before data that uses a new minor: a loader refuses an artifact whose minor is newer than its own, naming both.
  • A capability or its executing process is a release (capabilities.py).

Check a candidate directory with the ordinary load:

python -m model_authority check <dir>   # prints the health fact, or exits 1 with the refusal

Tests: .venv/bin/python3 -m pytest packages/model-authority/tests -q (make test-area AREA=model_authority).

Metadata

Release files for hank-model-authority 1.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hank-model-authority 1.5.0
File Size Uploaded
hank_model_authority-1.5.0.tar.gz 78.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hank-model-authority 1.5.0
File Interpreter ABI Platform
hank_model_authority-1.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 131.8 kB

Release files / hank_model_authority-1.5.0.tar.gz

Download URL hank_model_authority-1.5.0.tar.gz
Size 78.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5be44bf91868c136e1fdca351691e09850380a3c2124ae8de8a67e93fff271ed
BLAKE2b-256 checksum
How to use checksums
88ae87514708fd2988a0c5a1305607aa45fb38695dbf3022b9ff9707543d0f5e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / hank_model_authority-1.5.0-py3-none-any.whl

Download URL hank_model_authority-1.5.0-py3-none-any.whl
Size 53.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0892fe80742de4ff873a0dc5bc28b0cf5529e00f55ecca589162cbcb93e718b6
BLAKE2b-256 checksum
How to use checksums
1c45bfac6b46e3df73dc92e9fe8b5667b038342fbcbe59dfa4588e797c13c9b6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release history Release notifications | RSS feed

1.6.1

2 release files

1.6.0

2 release files

1.5.2

2 release files

1.5.1

2 release files

This release

1.5.0 This release

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page