Going further · 11
Model Catalog
The model catalog holds per-model data that riffer can’t derive from the model name: how a reasoning level maps to request params, and what the model costs. It lives in JSON files you list in your configuration, so a new model or a price change is a data edit, not a riffer release.
Riffer ships no catalog. Without one:
reasoningstill works through each provider’s default mapping (see Provider defaults).- Calls carry no cost (
token_usage.costisnil), unless the deprecatedconfig.pricingprices the model.
Loading catalog files
Riffer.configure do |config|
config.catalog_files = ["config/riffer/models.json"]
end
catalog_filestakes an Array of paths (Strings or Pathnames). Files load in the order listed.Riffer.configurebuilds the catalog at the end of the block, so a missing file or invalid data raisesRiffer::ArgumentErrorat boot.- Calling
configureagain rebuilds the catalog from scratch. - Files load at boot only. There is no API to add entries at runtime. The built catalog is frozen.
File format
{
"version": 1,
"models": {
"anthropic/claude-haiku-4-5-20251001": {
"aliases": ["anthropic/claude-haiku-4-5"],
"pricing": {
"input": 1.0,
"output": 5.0,
"cache_read": 0.1,
"cache_write": 1.25
},
"reasoning": {
"off": { "thinking": { "type": "disabled" } },
"low": {
"thinking": { "type": "enabled", "budget_tokens": 1024 },
"max_tokens": 5120
},
"high": {
"thinking": { "type": "enabled", "budget_tokens": 24576 },
"max_tokens": 28672
}
}
},
"gemini/gemini-2.5-flash": {
"reasoning": {
"off": { "thinkingConfig": { "thinkingBudget": 0 } },
"low": { "thinkingConfig": { "thinkingBudget": 1024 } }
}
},
"amazon_bedrock/us.amazon.nova-2-lite-v1:0": {
"reasoning": {
"low": {
"additional_model_request_fields": {
"reasoningConfig": {
"type": "enabled",
"maxReasoningEffort": "low"
}
}
}
}
}
}
}
| Key | Description |
|---|---|
version |
Required. The file format version. Only 1 is supported. |
models |
An object keyed by exact provider/model id: the same string you give an agent’s model. |
Each model entry can have any of these keys:
| Key | Description |
|---|---|
aliases |
Other provider/model ids that share this entry, pricing included. An alias may name a different provider, e.g. "azure_openai/my-gpt-prod" on an OpenAI entry. |
pricing |
Per-million-token rates. See Pricing. |
reasoning |
An object keyed by level (off, low, medium, high, xhigh, max). Each value is a model_options fragment. See Reasoning. |
Lookup
A model id matches an entry by its exact key first, then by an alias. There are no patterns, families, or fallback entries.
The provider part of the key is the provider’s repository identifier, so a custom provider registered as :my_provider reads entries under my_provider/....
Reasoning
A reasoning fragment is written exactly as you would write it in model_options, using the provider’s own param names. Bedrock fragments spell out additional_model_request_fields and inference_config themselves.
When an agent sets reasoning, riffer picks the params for that level in this order:
- The catalog entry’s fragment for the level (by exact key, then alias).
- The provider’s default mapping, when the entry is missing or doesn’t define the level.
- If the provider has no default either, riffer raises
Riffer::ArgumentErrorbefore the request:No reasoning mapping for my_provider/my-model at level :low. Add one in a catalog file (see docs/CATALOG.md).
Riffer doesn’t check whether a model supports a level. If the model rejects the params, you get the provider’s error unchanged.
Provider defaults
The defaults send the level name as the provider’s effort value:
| Provider | :off |
Other levels |
|---|---|---|
| OpenAI, Azure OpenAI | reasoning: "none" |
reasoning: "<level>" |
| OpenRouter | reasoning: "none" |
reasoning: "<level>" |
| Anthropic | thinking: { type: "disabled" } |
thinking: { type: "adaptive" }, output_config: { effort: "<level>" } |
| Amazon Bedrock | additional_model_request_fields: { thinking: { type: "disabled" } } |
additional_model_request_fields: { thinking: { type: "adaptive" }, output_config: { effort: "<level>" } } |
| Gemini | thinkingConfig: { thinkingLevel: "off" } |
thinkingConfig: { thinkingLevel: "<level>" } |
| Mock | nothing | nothing |
Add a catalog entry for models that need different params, for example:
- Claude 4.5 and earlier, which take a
budget_tokensthinking budget instead of adaptive thinking. - Gemini 2.5, which takes
thinkingBudget. - Non-Claude models on Amazon Bedrock (Nova, OpenAI), which use their own fields.
A custom provider can supply its own default.
Merging with model_options
The agent’s model_options always win. Riffer deep-merges them over the fragment: nested objects merge key by key, and any other value (including arrays) replaces the fragment’s. So you can override one field without repeating the rest:
class SummaryAgent < Riffer::Agent
model "anthropic/claude-haiku-4-5-20251001"
reasoning :low
model_options max_tokens: 8000 # keeps the catalog's thinking budget, replaces its max_tokens
end
Riffer doesn’t check the merged result. If a partial override makes the request invalid, the provider rejects it.
Pricing
{
"pricing": {
"input": 3.0,
"output": 15.0,
"cache_read": 0.3,
"cache_write": 3.75
}
}
| Key | Description |
|---|---|
input |
Required. Price per million input tokens. Applies to the uncached portion of input_tokens. |
output |
Required. Price per million output tokens. |
cache_read |
Price per million cache-read tokens. When omitted, cache reads bill at the input rate. |
cache_write |
Price per million cache-write tokens. When omitted, cache writes bill at the input rate. |
Because the cache buckets are subsets of input_tokens, the cost formula subtracts them before applying the input rate:
cost = (input − cache_read − cache_write) × input_rate
+ cache_read × cache_read_rate
+ cache_write × cache_write_rate
+ output × output_rate
(all rates ÷ 1,000,000; an unset cache rate falls back to input_rate.) Cost is for observability, not billing: it’s a Float, and sub-cent rounding can accumulate over a long run. See Messages → Token Usage Semantics for how cost surfaces and aggregates.
A model with no catalog pricing carries no cost. The deprecated config.pricing is still read as a fallback; catalog pricing wins when both price a model.
Layering files
Later files override earlier ones, so you can keep shared data in one file and local overrides in another:
- For an entry with the same key, a later file replaces whole sections (
pricing,reasoning) and leaves the others alone. aliasesarrays are combined, so a later file can add aliases without repeating earlier ones.- If a later file lists an alias an earlier file gave to a different entry, the later file wins.
{
"version": 1,
"models": {
"anthropic/claude-haiku-4-5-20251001": {
"pricing": { "input": 0.8, "output": 4.0 }
},
"openai/gpt-5.1": {
"aliases": ["azure_openai/my-gpt-prod"]
}
}
}
Listed after the first example, this file replaces Haiku’s pricing but keeps its reasoning, and routes an Azure deployment to the openai/gpt-5.1 entry.
Validation
Loading raises Riffer::ArgumentError, naming the file and the model key, when:
- The file is missing or isn’t valid JSON, or
versionis missing or unsupported. - The file has keys other than
versionandmodels, or an entry has keys other thanaliases,pricing, andreasoning. - A model key or alias isn’t in
provider/modelform, oraliasesisn’t an array of strings. pricingis missinginputoroutput, has unknown keys, or has a rate that isn’t a non-negative number.- A reasoning level isn’t one of
off,low,medium,high,xhigh,max, or its value isn’t an object. - Two entries in the same file list the same alias.
- After all files are layered, an alias is also a model key.