cadrumo.core.model_catalogue module

The typed local-model catalogue: what may be selected, and under whose licence.

One home for a fact that was previously invisible. Before this module the model identifiers lived as bare string defaults on Settings with their trade-offs described in field docstrings, and the licence axis was described nowhere at all – so the shipped vision default was a research-licensed model that bars commercial use, inside a commercial tax product, with nothing in the tree that could have said so.

Three declarations per candidate carry the weight:

memory_requirement_bytes

The publisher’s stated weight size, the figure assess_model_load_contention() compares against measured free headroom. It is the requirement BEFORE the configured safety margin, never after – the margin is deployment policy and lives on Settings.

max_context_tokens

The capability floor, and the reason selection is bounded from BELOW. The document read sends the registry allow-list prompt plus an encoded page, so a model whose window cannot hold cadrumo_llm_ollama_num_ctx is not a cheaper option – it is an unusable one, and it must be excluded on capability rather than ranked below on quality.

ModelLicence

An SPDX identifier plus an EXPLICIT commercial_use_permitted flag, each verified against the publisher’s own text at authoring and carrying the quote and URL that were read. An unverified licence is a refusal input, not a permissive default: ModelLicence refuses to be constructed as commercially usable without a verification source, so the failure direction of a future hand-edit is a build error rather than a silent legal claim.

This is deployment configuration, not registry data – it encodes no tax semantics – so it lives in core/ beside the Settings fields it supplies defaults to, and the selection logic that consumes it lives in the application layer at select_model_for_role().

See also

AcceleratorKind

The measured-hardware axis selection filters candidates against.

HardwareProfile

The measured profile whose free figures decide whether a candidate fits.

class ModelRole(*values)[source]

Bases: StrEnum

The distinct jobs a local model is selected for.

Kept as roles rather than as one “the local model” axis because the capability bars differ, and they differ in BOTH directions: only vision transcription needs image input, and the two text roles must be satisfiable on a machine that cannot host a vision model at all. Each role resolves independently through select_model_for_role().

The two text roles are not one role. Column-role mapping is strictly easier than text extraction – a selection over a short closed vocabulary given a handful of short header strings, against reading a whole document’s text layer – so it is kept separate precisely so it can be sized DOWN independently rather than inheriting the harder role’s model.

VISION_TRANSCRIPTION reads a scanned or photographed page; TEXT_EXTRACTION classifies an already-extracted text layer; COLUMN_ROLE_MAPPING names the columns of a delimited table from their header strings; and SUPPLY_NATURE_PROPOSAL proposes whether invoice lines supply goods or services for an operator to confirm.

VISION_TRANSCRIPTION
TEXT_EXTRACTION
COLUMN_ROLE_MAPPING
SUPPLY_NATURE_PROPOSAL
class ModelRuntime(*values)[source]

Bases: StrEnum

Where a candidate’s weights actually run, which changes what may be judged.

The axis exists because two of the catalogue’s bars are meaningless off-host. A hosted model has no local memory requirement to compare against measured free headroom, and no weights to pull; what it has instead is a service contract. Collapsing the two runtimes into one list would force every consumer to special-case that, and would let a cloud model be ranked by a memory figure it does not have.

The capability and licence bars still apply to both: the prompt is the same size wherever it runs, and “may we use this commercially” is the same question whether the answer comes from a weights licence or a terms of service.

Members:

LOCAL_OLLAMA: Weights pulled and run on the operator’s own machine. CLOUD_ANTHROPIC: A hosted Anthropic API model; nothing runs on-host.

LOCAL_OLLAMA
CLOUD_ANTHROPIC
class LicenceVerification(*values)[source]

Bases: StrEnum

How a catalogue entry’s licence claim was checked, and against what.

Recorded because the claim’s provenance is the part that can rot. An SPDX identifier written from recall reads identically to one read off the publisher’s licence file, and only one of the two survives a lawyer. The member names the artefact that was actually read.

PUBLISHER_LICENCE_FILE means the publisher’s LICENSE text was read; PUBLISHER_MODEL_CARD means its model-card licence field was read; PUBLISHER_SERVICE_TERMS means its service terms were read; and UNVERIFIED means no publisher text was read and therefore bars a commercial-use claim (see ModelLicence).

PUBLISHER_LICENCE_FILE
PUBLISHER_MODEL_CARD
PUBLISHER_SERVICE_TERMS
UNVERIFIED
class DeploymentLicencePosture(*values)[source]

Bases: StrEnum

Whether this deployment needs a licence that permits commercial use.

COMMERCIAL is the product’s posture: Cadrumo is a commercial tax product and a gestor filing for clients is unambiguously commercial use. NON_COMMERCIAL exists so an individual filing their own return, or a research evaluation, is not refused a candidate whose licence genuinely covers them – the posture is a fact about the deployment, and encoding it as one is what keeps the commercial default honest rather than merely strict.

Members:

COMMERCIAL: Only candidates whose licence permits commercial use. NON_COMMERCIAL: Research-licensed candidates are additionally eligible.

COMMERCIAL
NON_COMMERCIAL
class ModelSelectionAdvisory(*values)[source]

Bases: StrEnum

Why a resolved selection is worth telling the operator about.

Selection never fails silently and an override is never honoured silently. Each member keys one operator-visible statement, so a caller renders the reason rather than re-deriving it from the selection’s shape.

Members:

LICENCE_COMMERCIAL_USE_BARRED: The selected model’s licence bars

commercial use under the active posture. Only reachable through an explicit override; automatic selection excludes such candidates.

LICENCE_UNVERIFIED: No publisher licence text backs the selected model,

so no licence claim can be made about it at all.

OVERRIDE_NOT_IN_CATALOGUE: The operator named a model the catalogue does

not describe, so neither its licence nor its fit could be judged.

OVERRIDE_BELOW_CONTEXT_FLOOR: The selected model’s context window is

smaller than the configured request window.

FIT_EXCEEDS_MEASURED_HEADROOM: The selected model’s requirement plus the

safety margin exceeds measured free memory.

FIT_UNVERIFIED: Free memory could not be measured, so fit was not

checked here. The load itself still fails closed at assess_model_load_contention().

LICENCE_COMMERCIAL_USE_BARRED
LICENCE_UNVERIFIED
OVERRIDE_NOT_IN_CATALOGUE
OVERRIDE_BELOW_CONTEXT_FLOOR
FIT_EXCEEDS_MEASURED_HEADROOM
FIT_UNVERIFIED
class ModelLicence(**data)[source]

Bases: BaseModel

One model’s licence, its commercial-use consequence, and the text that proves it.

commercial_use_permitted is declared explicitly rather than derived from spdx_id because the derivation is exactly the step that goes wrong: a reader who knows Apache-2.0 permits commercial use will assume a publisher-specific LicenseRef-* does too, and one of this catalogue’s entries is a licence whose text says “FOR NON-COMMERCIAL PURPOSES ONLY”.

verified_quote and source_url are required whenever a verification source is claimed, so the claim can be re-checked by opening one URL. The validator enforces the asymmetry that matters: an UNVERIFIED licence may not assert commercial use. Unverified is a refusal input, never a permissive default.

Parameters:
  • spdx_id (str)

  • name (str)

  • commercial_use_permitted (bool)

  • verification (LicenceVerification)

  • source_url (Annotated[str, StringConstraints(strip_whitespace=None, to_upper=None, to_lower=None, strict=None, min_length=None, max_length=1024, pattern=None, ascii_only=None)])

  • verified_quote (str)

spdx_id: str
name: str
commercial_use_permitted: bool
verification: LicenceVerification
source_url: OptionalSourceUrl
verified_quote: str
class ModelCandidate(**data)[source]

Bases: BaseModel

One selectable local model: what it costs, what it can hold, and its licence.

roles is a set because a vision-capable model serves both the transcription and the tabular-mapping roles from one pulled copy, and duplicating the row per role would let the two descriptions of the same weights drift.

measured_baseline_ref names the corpus measurement that justifies this candidate’s place in the ordering, or is empty where none has been run yet. Empty is the honest state for most of this catalogue today and must stay distinguishable from a measured zero – a candidate carrying no baseline is selected on its declared capability bars alone.

Parameters:
runtime_id: str
runtime: ModelRuntime
roles: frozenset[ModelRole]
memory_requirement_bytes: int | None
input_price_per_mtok_usd: Decimal | None
max_context_tokens: int
licence: ModelLicence
measured_baseline_ref: str
notes: str
property selection_rank: Decimal

Return the ascending ‘weakest first’ key for this candidate’s runtime.

Memory bytes on-host, published input price off-host. Comparable only within one runtime, which is why candidates_for_role() is scoped to one.

serves(role)[source]

Return whether this candidate is eligible for role.

Return type:

bool

Parameters:

role (ModelRole)

permitted_under(posture)[source]

Return whether this candidate’s licence permits posture.

A non-commercial posture accepts every catalogued licence; a commercial posture accepts only those whose publisher text was read AND permits commercial use.

Return type:

bool

Parameters:

posture (DeploymentLicencePosture)

APACHE_2_0: Final

Apache-2.0 as declared on the publishers’ own model cards.

Read from the license: frontmatter field of each covered model’s card at authoring; source_url names one of them and the remainder are recorded on each candidate’s notes. Apache-2.0 is an SPDX-listed permissive licence whose grant is explicitly commercial-capable.

QWEN_RESEARCH: Final

The Qwen Research licence, whose text bars commercial use outright.

Not an SPDX-listed licence, so it carries an SPDX LicenseRef- identifier rather than a fabricated listed one. The publisher’s licence file adds “If you are commercially using the Materials, you shall request a license from us”, which is what makes a barred candidate reachable only through an explicit, advised operator override rather than through automatic selection.

ANTHROPIC_COMMERCIAL_TERMS: Final

The service contract a hosted Anthropic model is used under.

Not a weights licence – there are no weights to license – so it carries an SPDX LicenseRef- identifier and is verified against the publisher’s terms of service rather than a LICENSE file. The same terms assign output ownership to the customer, which is what makes a hosted read usable in a filing-grade product at all.

Reading it as a licence keeps ONE gate over both runtimes: “may we use this commercially” has to be answered for a hosted model exactly as for a local one, and a second, parallel notion of permission is how one of the two answers goes unchecked.

DEFAULT_MODEL_BY_RUNTIME_AND_ROLE: Final[Mapping[ModelRuntime, Mapping[ModelRole, str]]]

Every shipped default, keyed by runtime then role – the source of the settings defaults.

Keyed by runtime as well as role because the two runtimes are configured separately and a role’s answer differs between them. Declared here rather than as literals on Settings so the licence gate has ONE place to read every shipped default from, across both runtimes, and so a default cannot name a model the catalogue does not describe.

That completeness is the point. A per-role gate that only knew about local defaults would have passed while every cloud route fell through to a frontier-tier model, which is exactly the state this mapping was added to correct.

candidates_for_role(role, runtime=ModelRuntime.LOCAL_OLLAMA)[source]

Return every candidate serving role on runtime, weakest first.

Scoped to one runtime because the ordering is only meaningful within one: ascending declared memory requirement ranks local candidates, and a hosted candidate has no such figure to be ranked by. Mixing them would sort a cloud model against a number it does not have.

Within a runtime the order IS the selection order: the design point is the weakest model that clears the capability bars, never the strongest the hardware or the budget could reach. A caller that wants a larger model names it as an override.

Return type:

tuple[ModelCandidate, ...]

Parameters:
model_candidate(runtime_id)[source]

Return the catalogued candidate with runtime_id, or None.

None means “this catalogue makes no claim about that model”, which is distinct from a claim that it is unlicensed – an operator naming an uncatalogued model is advised, not refused.

Return type:

ModelCandidate | None

Parameters:

runtime_id (str)

default_model_runtime_id(role, runtime=ModelRuntime.LOCAL_OLLAMA)[source]

Return the shipped default runtime id for role on runtime.

Supplies the Settings field defaults for both runtimes, so the settings surface and the licence gate read one value.

Return type:

str

Parameters: