feat: reasoning token budget + session name in header
- reasoning_budget_tokens: optional int field on local_openai models;
when set, injects {"reasoning": {"budget_tokens": N}} via extra_body
into every OpenRouter API call (both tool-loop and confirmation-gate
rounds). Field exposed in the model edit form in Settings.
- session name moved from standalone full-row div between #messages
and #input-area into the persona-switcher block in the header, as a
third dim line under "Cortex · Local". Collapses when empty via
:empty CSS. No JS changes required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -664,7 +664,8 @@ def save_model(username: str, model_id: str | None, host_id: str,
|
|||||||
label: str, model_name: str, context_k: int = 0,
|
label: str, model_name: str, context_k: int = 0,
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
max_rounds: int | None = None,
|
max_rounds: int | None = None,
|
||||||
tools: bool = True) -> str:
|
tools: bool = True,
|
||||||
|
reasoning_budget_tokens: int | None = None) -> str:
|
||||||
"""Create or update a local_openai model entry. Returns the model ID."""
|
"""Create or update a local_openai model entry. Returns the model ID."""
|
||||||
data = _load(username)
|
data = _load(username)
|
||||||
tags = tags or []
|
tags = tags or []
|
||||||
@@ -672,29 +673,31 @@ def save_model(username: str, model_id: str | None, host_id: str,
|
|||||||
if model_id:
|
if model_id:
|
||||||
for m in data["models"]:
|
for m in data["models"]:
|
||||||
if m["id"] == model_id:
|
if m["id"] == model_id:
|
||||||
m["host_id"] = host_id
|
m["host_id"] = host_id
|
||||||
m["label"] = label.strip() or model_name.strip()
|
m["label"] = label.strip() or model_name.strip()
|
||||||
m["model_name"] = model_name.strip()
|
m["model_name"] = model_name.strip()
|
||||||
m["context_k"] = context_k
|
m["context_k"] = context_k
|
||||||
m["max_rounds"] = max_rounds
|
m["max_rounds"] = max_rounds
|
||||||
m["tools"] = tools
|
m["tools"] = tools
|
||||||
m["tags"] = tags
|
m["tags"] = tags
|
||||||
|
m["reasoning_budget_tokens"] = reasoning_budget_tokens
|
||||||
_save(username, data)
|
_save(username, data)
|
||||||
return model_id
|
return model_id
|
||||||
model_id = None
|
model_id = None
|
||||||
|
|
||||||
model_id = secrets.token_hex(4)
|
model_id = secrets.token_hex(4)
|
||||||
data["models"].append({
|
data["models"].append({
|
||||||
"id": model_id,
|
"id": model_id,
|
||||||
"type": "local_openai",
|
"type": "local_openai",
|
||||||
"label": label.strip() or model_name.strip(),
|
"label": label.strip() or model_name.strip(),
|
||||||
"model_name": model_name.strip(),
|
"model_name": model_name.strip(),
|
||||||
"provider": "local",
|
"provider": "local",
|
||||||
"host_id": host_id,
|
"host_id": host_id,
|
||||||
"context_k": context_k,
|
"context_k": context_k,
|
||||||
"max_rounds": max_rounds,
|
"max_rounds": max_rounds,
|
||||||
"tools": tools,
|
"tools": tools,
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
|
"reasoning_budget_tokens": reasoning_budget_tokens,
|
||||||
})
|
})
|
||||||
_save(username, data)
|
_save(username, data)
|
||||||
return model_id
|
return model_id
|
||||||
|
|||||||
@@ -287,6 +287,9 @@ async def _run_from_messages(
|
|||||||
if active_tools:
|
if active_tools:
|
||||||
call_kwargs["tools"] = active_tools
|
call_kwargs["tools"] = active_tools
|
||||||
call_kwargs["tool_choice"] = "auto"
|
call_kwargs["tool_choice"] = "auto"
|
||||||
|
reasoning_budget = (model_cfg or {}).get("reasoning_budget_tokens")
|
||||||
|
if reasoning_budget:
|
||||||
|
call_kwargs["extra_body"] = {"reasoning": {"budget_tokens": reasoning_budget}}
|
||||||
response = await _chat_with_retry(client, **call_kwargs)
|
response = await _chat_with_retry(client, **call_kwargs)
|
||||||
|
|
||||||
choice = response.choices[0]
|
choice = response.choices[0]
|
||||||
@@ -346,6 +349,8 @@ async def _run_from_messages(
|
|||||||
conf_call: dict = {"model": model_name, "messages": messages, "tool_choice": "none"}
|
conf_call: dict = {"model": model_name, "messages": messages, "tool_choice": "none"}
|
||||||
if active_tools:
|
if active_tools:
|
||||||
conf_call["tools"] = active_tools
|
conf_call["tools"] = active_tools
|
||||||
|
if reasoning_budget:
|
||||||
|
conf_call["extra_body"] = {"reasoning": {"budget_tokens": reasoning_budget}}
|
||||||
conf_resp = await _chat_with_retry(client, **conf_call)
|
conf_resp = await _chat_with_retry(client, **conf_call)
|
||||||
final_response = conf_resp.choices[0].message.content or (
|
final_response = conf_resp.choices[0].message.content or (
|
||||||
"This action requires your explicit confirmation before it can proceed."
|
"This action requires your explicit confirmation before it can proceed."
|
||||||
|
|||||||
@@ -204,12 +204,13 @@ def _render(username: str, success: str = "", error: str = "") -> str:
|
|||||||
else:
|
else:
|
||||||
extra_fields = '<input type="hidden" name="credential_id" value="cli">'
|
extra_fields = '<input type="hidden" name="credential_id" value="cli">'
|
||||||
|
|
||||||
cur_label = m.get("label", "")
|
cur_label = m.get("label", "")
|
||||||
cur_model_name = m.get("model_name", "")
|
cur_model_name = m.get("model_name", "")
|
||||||
cur_ctx = m.get("context_k", 0) or 0
|
cur_ctx = m.get("context_k", 0) or 0
|
||||||
cur_max_rounds = m.get("max_rounds") or 0
|
cur_max_rounds = m.get("max_rounds") or 0
|
||||||
cur_tools = m.get("tools", True)
|
cur_tools = m.get("tools", True)
|
||||||
cur_tags = ", ".join(m.get("tags") or [])
|
cur_tags = ", ".join(m.get("tags") or [])
|
||||||
|
cur_reasoning_budget = m.get("reasoning_budget_tokens") or 0
|
||||||
|
|
||||||
model_rows += f'''
|
model_rows += f'''
|
||||||
<div class="model-row" id="model-{m["id"]}">
|
<div class="model-row" id="model-{m["id"]}">
|
||||||
@@ -256,6 +257,11 @@ def _render(username: str, success: str = "", error: str = "") -> str:
|
|||||||
<input type="number" name="max_rounds" value="{cur_max_rounds}" min="0"
|
<input type="number" name="max_rounds" value="{cur_max_rounds}" min="0"
|
||||||
title="Per-model tool loop cap. 0 = use the global default (orchestrator_max_rounds).">
|
title="Per-model tool loop cap. 0 = use the global default (orchestrator_max_rounds).">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field" style="flex:0 0 auto">
|
||||||
|
<label title="OpenRouter reasoning budget in tokens. 0 = no reasoning override (model default). Injects reasoning.budget_tokens into the API call.">Reasoning tokens</label>
|
||||||
|
<input type="number" name="reasoning_budget_tokens" value="{cur_reasoning_budget}" min="0"
|
||||||
|
title="OpenRouter reasoning budget in tokens. 0 = disabled. E.g. 2048 for light thinking, 8192 for deep reasoning.">
|
||||||
|
</div>
|
||||||
<div class="field" style="flex:0 0 auto">
|
<div class="field" style="flex:0 0 auto">
|
||||||
<label title="Whether this model supports tool calling. If not supported, requests skip the tool loop entirely.">Tool calling</label>
|
<label title="Whether this model supports tool calling. If not supported, requests skip the tool loop entirely.">Tool calling</label>
|
||||||
<select name="tools"
|
<select name="tools"
|
||||||
@@ -459,20 +465,21 @@ async def remove_host(request: Request, host_id: str):
|
|||||||
|
|
||||||
@router.post("/settings/local/models/add", include_in_schema=False)
|
@router.post("/settings/local/models/add", include_in_schema=False)
|
||||||
async def add_model(
|
async def add_model(
|
||||||
request: Request,
|
request: Request,
|
||||||
provider: str = Form("local"),
|
provider: str = Form("local"),
|
||||||
label: str = Form(""),
|
label: str = Form(""),
|
||||||
context_k: int = Form(0),
|
context_k: int = Form(0),
|
||||||
max_rounds: int = Form(0),
|
max_rounds: int = Form(0),
|
||||||
tools: int = Form(1),
|
tools: int = Form(1),
|
||||||
tags: str = Form(""),
|
tags: str = Form(""),
|
||||||
|
reasoning_budget_tokens: int = Form(0),
|
||||||
# local-only fields
|
# local-only fields
|
||||||
host_id: str = Form(""),
|
host_id: str = Form(""),
|
||||||
model_name: str = Form(""),
|
model_name: str = Form(""),
|
||||||
# cloud-only fields
|
# cloud-only fields
|
||||||
cloud_model_name: str = Form(""),
|
cloud_model_name: str = Form(""),
|
||||||
account_id: str = Form(""),
|
account_id: str = Form(""),
|
||||||
credential_id: str = Form("cli"),
|
credential_id: str = Form("cli"),
|
||||||
):
|
):
|
||||||
username = _get_user(request)
|
username = _get_user(request)
|
||||||
if not username:
|
if not username:
|
||||||
@@ -481,6 +488,7 @@ async def add_model(
|
|||||||
tag_list = [t.strip() for t in tags.split(",") if t.strip()]
|
tag_list = [t.strip() for t in tags.split(",") if t.strip()]
|
||||||
max_rounds_ = max_rounds or None
|
max_rounds_ = max_rounds or None
|
||||||
tools_bool = tools != 0
|
tools_bool = tools != 0
|
||||||
|
reasoning_budget_ = reasoning_budget_tokens or None
|
||||||
|
|
||||||
if provider == "local":
|
if provider == "local":
|
||||||
if not model_name.strip():
|
if not model_name.strip():
|
||||||
@@ -488,7 +496,8 @@ async def add_model(
|
|||||||
if not host_id.strip():
|
if not host_id.strip():
|
||||||
return HTMLResponse(_render(username, error="Select a host."))
|
return HTMLResponse(_render(username, error="Select a host."))
|
||||||
reg.save_model(username, None, host_id, label, model_name, context_k, tag_list,
|
reg.save_model(username, None, host_id, label, model_name, context_k, tag_list,
|
||||||
max_rounds=max_rounds_, tools=tools_bool)
|
max_rounds=max_rounds_, tools=tools_bool,
|
||||||
|
reasoning_budget_tokens=reasoning_budget_)
|
||||||
display = label or model_name
|
display = label or model_name
|
||||||
|
|
||||||
elif provider in ("google", "anthropic"):
|
elif provider in ("google", "anthropic"):
|
||||||
@@ -513,32 +522,35 @@ async def add_model(
|
|||||||
|
|
||||||
@router.post("/settings/local/models/{model_id}/edit", include_in_schema=False)
|
@router.post("/settings/local/models/{model_id}/edit", include_in_schema=False)
|
||||||
async def edit_model(
|
async def edit_model(
|
||||||
request: Request,
|
request: Request,
|
||||||
model_id: str,
|
model_id: str,
|
||||||
mtype: str = Form(""),
|
mtype: str = Form(""),
|
||||||
label: str = Form(""),
|
label: str = Form(""),
|
||||||
model_name: str = Form(""),
|
model_name: str = Form(""),
|
||||||
context_k: int = Form(0),
|
context_k: int = Form(0),
|
||||||
max_rounds: int = Form(0),
|
max_rounds: int = Form(0),
|
||||||
tools: int = Form(1),
|
tools: int = Form(1),
|
||||||
tags: str = Form(""),
|
tags: str = Form(""),
|
||||||
host_id: str = Form(""),
|
reasoning_budget_tokens: int = Form(0),
|
||||||
account_id: str = Form(""),
|
host_id: str = Form(""),
|
||||||
credential_id: str = Form("cli"),
|
account_id: str = Form(""),
|
||||||
|
credential_id: str = Form("cli"),
|
||||||
):
|
):
|
||||||
username = _get_user(request)
|
username = _get_user(request)
|
||||||
if not username:
|
if not username:
|
||||||
return RedirectResponse("/login", status_code=302)
|
return RedirectResponse("/login", status_code=302)
|
||||||
if not model_name.strip():
|
if not model_name.strip():
|
||||||
return HTMLResponse(_render(username, error="Model name is required."))
|
return HTMLResponse(_render(username, error="Model name is required."))
|
||||||
tag_list = [t.strip() for t in tags.split(",") if t.strip()]
|
tag_list = [t.strip() for t in tags.split(",") if t.strip()]
|
||||||
max_rounds_ = max_rounds or None
|
max_rounds_ = max_rounds or None
|
||||||
tools_bool = tools != 0
|
tools_bool = tools != 0
|
||||||
|
reasoning_budget_ = reasoning_budget_tokens or None
|
||||||
if mtype == "local_openai":
|
if mtype == "local_openai":
|
||||||
if not host_id.strip():
|
if not host_id.strip():
|
||||||
return HTMLResponse(_render(username, error="Select a host for this model."))
|
return HTMLResponse(_render(username, error="Select a host for this model."))
|
||||||
reg.save_model(username, model_id, host_id, label, model_name, context_k, tag_list,
|
reg.save_model(username, model_id, host_id, label, model_name, context_k, tag_list,
|
||||||
max_rounds=max_rounds_, tools=tools_bool)
|
max_rounds=max_rounds_, tools=tools_bool,
|
||||||
|
reasoning_budget_tokens=reasoning_budget_)
|
||||||
elif mtype == "gemini_api":
|
elif mtype == "gemini_api":
|
||||||
reg.save_cloud_model(username, model_id, "google", model_name, label,
|
reg.save_cloud_model(username, model_id, "google", model_name, label,
|
||||||
account_id=account_id or None, context_k=context_k, tags=tag_list,
|
account_id=account_id or None, context_k=context_k, tags=tag_list,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
<div class="persona-switcher" id="persona-switcher">
|
<div class="persona-switcher" id="persona-switcher">
|
||||||
<div class="name" id="persona-name">Inara</div>
|
<div class="name" id="persona-name">Inara</div>
|
||||||
<div class="subtitle">Cortex · Local</div>
|
<div class="subtitle">Cortex · Local</div>
|
||||||
|
<div id="session-id"></div>
|
||||||
<div class="persona-dropdown" id="persona-dropdown"></div>
|
<div class="persona-dropdown" id="persona-dropdown"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -164,7 +165,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="messages"></div>
|
<div id="messages"></div>
|
||||||
<div id="session-id"></div>
|
|
||||||
|
|
||||||
<div id="input-area">
|
<div id="input-area">
|
||||||
<!-- Mode select — compact dropdown, opens upward, MRU sorted -->
|
<!-- Mode select — compact dropdown, opens upward, MRU sorted -->
|
||||||
|
|||||||
@@ -897,11 +897,14 @@
|
|||||||
#stop:hover { background: #5c1a1a; }
|
#stop:hover { background: #5c1a1a; }
|
||||||
|
|
||||||
#session-id {
|
#session-id {
|
||||||
font-size: 0.7rem;
|
font-size: 0.68rem;
|
||||||
color: var(--border);
|
color: var(--border);
|
||||||
padding: 0 20px 6px;
|
white-space: nowrap;
|
||||||
background: var(--surface);
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 220px;
|
||||||
}
|
}
|
||||||
|
#session-id:empty { display: none; }
|
||||||
|
|
||||||
/* ── Message wrappers (edit/delete controls) ──────────────── */
|
/* ── Message wrappers (edit/delete controls) ──────────────── */
|
||||||
.msg-wrapper {
|
.msg-wrapper {
|
||||||
|
|||||||
Reference in New Issue
Block a user