Reasoning effort
Give callers four effort levels that mean the same on every chat model, and let one model name pick a different model for each level.
6 min readUpdated for v2.4.0Edit on GitHub
none, low, medium, highCallers ask for reasoning effort as none, low, medium or high, but model servers disagree about what those words mean: one has only low and high, another turns thinking off with a chat-template flag, a third rejects high outright. A reasoning map tells obleth what each level becomes for one model's server, and a model family sends each level to a different model.
Translate effort for a model
- 1
Open the model's Reasoning section
On a chat model's page, open Reasoning and set Effort on requests to Translate the four levels.
- 2
Say what each level sends
Under What each level sends, pick one per level: Server default (send nothing), Send reasoning_effort = a value, Turn thinking off or Turn thinking on (
chat_template_kwargs.enable_thinking), Same as another level, or Custom fields (JSON). Then set When a caller sets no effort: Leave it to the model's server, or serve it as one of the levels. - 3
Check the preview and save
What requests become previews each level. Choose Save reasoning. The section saves on its own, not with the save bar, and applies within a few seconds.
Or through the Management API. This model turns thinking off with a chat-template flag, takes low and high, and runs at its server's own default for medium:
curl -X PUT http://localhost:9180/api/v1/models/$MODEL_ID/reasoning \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "mapped",
"default_level": "none",
"levels": {
"none": {"send": {"chat_template_kwargs": {"enable_thinking": false}}},
"low": {"send": {"reasoning_effort": "low"}},
"medium": {"send": {}},
"high": {"send": {"reasoning_effort": "high"}}
}
}'
The body replaces the whole map. {"mode": "passthrough"} turns translation off.
How a level is applied
mode | In the dashboard | What the model's server gets |
|---|---|---|
passthrough (default) | Pass through | The request as the caller sent it, effort included |
mapped | Translate the four levels | The caller's reasoning_effort and reasoning removed, and the level's fields merged in |
unsupported | This model has no effort control | The caller's reasoning_effort and reasoning removed, so a server that would reject them never sees them |
A mapped model needs an action for every level:
{"send": {…}}merges those fields into the request. Objects merge key by key and your value wins, so a caller's otherchat_template_kwargskeys stay.{"send": {}}sends nothing, which leaves the server's own default.{"use": "<level>"}serves the request as another level, for a model that lacks this one, such as"none": {"use": "low"}on a model with no off switch. It must name a level that sends.default_levelis the level served when the caller names none. Without it, such a request goes as it came.
Ask for a level
| API | Where effort is read |
|---|---|
| Chat completions | reasoning_effort, or reasoning.effort |
| Responses | reasoning.effort |
| Anthropic Messages | output_config.effort; else thinking: disabled is none, and an enabled budget under 4,096 tokens is low, under 16,384 medium, and 16,384 or more (or unset) high. adaptive names no level |
minimal reads as low, xhigh and max as high, and off and disabled as none. Any other value is refused with 400 by a mapped or unsupported model and by a family; a pass-through model gets it as sent. The Responses and Messages translations to chat have no field for effort, so on those APIs effort reaches a model only through a map or a family.
curl -si http://localhost:8088/v1/chat/completions \
-H "Authorization: Bearer $OBLETH_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "acme",
"reasoning_effort": "high",
"messages": [{"role": "user", "content": "Is 2^61 - 1 prime?"}]
}'
What callers see
| Response header | Value |
|---|---|
x-obleth-reasoning-effort | The level served, or unsupported when the caller's effort was removed. Absent for a pass-through model, and when no level was named and no default applied |
x-obleth-reasoning-effort-requested | The level the caller asked for, sent only when it was served as another |
A mapped model's response cache entries are kept apart per level served.
/v1/models and GET /v1/models/{id} give a mapped or unsupported model a reasoning object, and /model/info has the same under model_info.reasoning. levels says which level each one is served at, so a client can see that none means low here. An unsupported model shows {"mode": "unsupported"}; a pass-through model has no reasoning.
"reasoning": {
"mode": "mapped",
"levels": {"none": "low", "low": "low", "medium": "medium", "high": "high"},
"default_level": "medium"
}
Model families
A family is one name for several chat models, with the level picking which one answers: acme-instruct for none and acme-think for the rest, all called as acme. A request that names no level gets the family's default level.
On the Models page, open Model families › Add family. Enter the Name callers send, an optional Description, the model for each level under Which model answers each level, and When a caller sets no effort. The preview says where requests go. Or:
curl -X POST http://localhost:9180/api/v1/model-families \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "acme",
"description": "Instruct for quick answers, thinking for the rest.",
"default_level": "none",
"members": {
"none": "acme-instruct",
"low": "acme-think",
"medium": "acme-think",
"high": "acme-think"
}
}'
GET /api/v1/model-families lists families, PUT /api/v1/model-families/{id} replaces one whole and DELETE removes it. Gateways pick up a change within 15 seconds.
The member serves the request as if the caller had named it: its pool, prices, lifecycle and allowlist entry, and a tenant allowed the family's name may call it too. The level is passed on, so the member's own map applies. Set a member that has no effort control to This model has no effort control, so the caller's reasoning_effort is removed instead of reaching a server that may refuse it.
- Names. A family name shares one namespace with model names, aliases and variants. A model that answers for a family can't be deleted until the family changes.
- Responses.
x-obleth-model-variantcarries the family's name, andx-obleth-reasoning-effortthe level served. - Discovery.
/v1/modelslists a family while its default member is enabled and active or deprecated, with afamilyobject (default_level,members) and the default member's context window, prices, capabilities and boons. A tenant with an allowlist sees it when the family, or every member, is allowed. - Verdicts.
/v1/verdictsanswers a family name with itsnonemember.
Keep members out of the listing
A family stands for its members, but callers would still see acme, acme-instruct and acme-think in /v1/models. On each member's page, turn off List in /v1/models under General and save with the save bar. Or:
curl -X PUT http://localhost:9180/api/v1/models/$MODEL_ID/listing \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"listed": false}'
An unlisted model and its variants are left out of /v1/models and /model/info only. They still answer to every name and to GET /v1/models/{id}, stay auto candidates and can be family members. Staging would also keep the model from auto.
Request logs
| Usage field | Holds |
|---|---|
reasoning_effort | The level served, unsupported when the caller's effort was removed, or empty when the request went through as sent |
reasoning_effort_requested | What the caller asked for, folded to the four levels when obleth knows the value; recorded for pass-through models too |
model_family | The family the caller named. model is the member that answered, and model_variant stays empty |
A request's panel in Request logs shows Called as ("acme, a model family. The reasoning level picked acme-think to answer.") and Reasoning effort in a sentence, such as "Asked for none, served as low: this model has no none level." The list adds "via acme" and "high effort" under the model. The columns are in ClickHouse usage.
Rules for a reasoning map
- Only chat models take a map other than pass-through.
- A mapped model needs all four levels. A
usecan't name its own level or a level that itself uses another. - A level may not set
model,messages,prompt,input,stream,stream_options,tools,tool_choiceorn, and may send at most 4,096 bytes of fields. passthroughandunsupportedkeep any levels sent with them, so switching back restores them.- Maps and
listedtravel in the model manifest and in backups.
Rules for a family
- The name is at most 128 characters with no spaces, and can't be
auto, any model's name, alias or variant, or another family's name. - Every level needs a member. A member is given by name or alias, stored as its name, and must be a registered chat model.
default_levelis required;descriptionis optional, up to 500 characters.- Families are carried in backups.
