1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-09-09 09:19:54 +00:00

reasoning effort control

This commit is contained in:
Laura
2025-08-10 22:32:40 +02:00
parent 92be3fdd85
commit 0f9fe50781
8 changed files with 137 additions and 24 deletions

BIN
.github/chat.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@@ -17,10 +17,10 @@ whiskr is a private, self-hosted web chat interface for interacting with AI mode
- Tags indicate if a model supports **tools**, **vision**, or **reasoning** - Tags indicate if a model supports **tools**, **vision**, or **reasoning**
- Search field with fuzzy matching to quickly find models - Search field with fuzzy matching to quickly find models
- Models are listed newest -> oldest - Models are listed newest -> oldest
- Reasoning effort control
## TODO ## TODO
- Reasoning effort control
- Retry button for assistant messages - Retry button for assistant messages
- Import and export of chats - Import and export of chats
- Web search tool - Web search tool

27
chat.go
View File

@@ -16,10 +16,16 @@ type Message struct {
Text string `json:"text"` Text string `json:"text"`
} }
type Reasoning struct {
Effort string `json:"effort"`
Tokens int `json:"tokens"`
}
type Request struct { type Request struct {
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Model string `json:"model"` Model string `json:"model"`
Temperature float64 `json:"temperature"` Temperature float64 `json:"temperature"`
Reasoning Reasoning `json:"reasoning"`
Messages []Message `json:"messages"` Messages []Message `json:"messages"`
} }
@@ -39,6 +45,21 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
request.Temperature = float32(r.Temperature) request.Temperature = float32(r.Temperature)
if model.Reasoning {
request.Reasoning = &openrouter.ChatCompletionReasoning{}
switch r.Reasoning.Effort {
case "high", "medium", "low":
request.Reasoning.Effort = &r.Reasoning.Effort
default:
if r.Reasoning.Tokens <= 0 || r.Reasoning.Tokens > 1024*1024 {
return nil, fmt.Errorf("invalid reasoning tokens (1-1048576): %d", r.Reasoning.Tokens)
}
request.Reasoning.MaxTokens = &r.Reasoning.Tokens
}
}
prompt, err := BuildPrompt(r.Prompt, model) prompt, err := BuildPrompt(r.Prompt, model)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -61,12 +82,6 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
}) })
} }
h := "high"
request.Reasoning = &openrouter.ChatCompletionReasoning{
Effort: &h,
}
return &request, nil return &request, nil
} }

View File

@@ -13,6 +13,8 @@ type Model struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
Reasoning bool `json:"-"`
} }
var ModelMap = make(map[string]*Model) var ModelMap = make(map[string]*Model)
@@ -38,11 +40,15 @@ func LoadModels() ([]*Model, error) {
name = name[index+2:] name = name[index+2:]
} }
tags, reasoning := GetModelTags(model)
m := &Model{ m := &Model{
ID: model.ID, ID: model.ID,
Name: name, Name: name,
Description: model.Description, Description: model.Description,
Tags: GetModelTags(model), Tags: tags,
Reasoning: reasoning,
} }
models[index] = m models[index] = m
@@ -53,10 +59,17 @@ func LoadModels() ([]*Model, error) {
return models, nil return models, nil
} }
func GetModelTags(model openrouter.Model) []string { func GetModelTags(model openrouter.Model) ([]string, bool) {
var tags []string var (
reasoning bool
tags []string
)
for _, parameter := range model.SupportedParameters { for _, parameter := range model.SupportedParameters {
if parameter == "reasoning" {
reasoning = true
}
if parameter == "reasoning" || parameter == "tools" { if parameter == "reasoning" || parameter == "tools" {
tags = append(tags, parameter) tags = append(tags, parameter)
} }
@@ -70,5 +83,5 @@ func GetModelTags(model openrouter.Model) []string {
sort.Strings(tags) sort.Strings(tags)
return tags return tags, reasoning
} }

View File

@@ -87,6 +87,10 @@ body {
pointer-events: none !important; pointer-events: none !important;
} }
.none {
display: none !important;
}
#messages { #messages {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -432,9 +436,10 @@ select {
padding: 2px 4px; padding: 2px 4px;
} }
#reasoning-tokens,
#temperature { #temperature {
appearance: textfield; appearance: textfield;
width: 50px; width: 48px;
padding: 2px 4px; padding: 2px 4px;
text-align: right; text-align: right;
} }
@@ -455,6 +460,14 @@ label[for="temperature"] {
background-image: url(icons/temperature.svg); background-image: url(icons/temperature.svg);
} }
label[for="reasoning-effort"] {
background-image: url(icons/reasoning.svg);
}
label[for="reasoning-tokens"] {
background-image: url(icons/amount.svg);
}
#bottom { #bottom {
top: -38px; top: -38px;
left: 50%; left: 50%;

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 806 B

View File

@@ -23,12 +23,12 @@
<textarea id="message" placeholder="Type something..." autocomplete="off"></textarea> <textarea id="message" placeholder="Type something..." autocomplete="off"></textarea>
<button id="add" title="Add message"></button> <button id="add" title="Add message to chat"></button>
<button id="send" title="Add message and start completion"></button> <button id="send" title="Add message to chat and start completion"></button>
<div class="options"> <div class="options">
<div class="option"> <div class="option">
<label for="role" title="Role"></label> <label for="role" title="Message role"></label>
<select id="role"> <select id="role">
<option value="user" selected>User</option> <option value="user" selected>User</option>
<option value="assistant">Assistant</option> <option value="assistant">Assistant</option>
@@ -40,7 +40,7 @@
<select id="model" data-searchable></select> <select id="model" data-searchable></select>
</div> </div>
<div class="option"> <div class="option">
<label for="prompt" title="Prompt"></label> <label for="prompt" title="Main system prompt"></label>
<select id="prompt"> <select id="prompt">
<option value="" selected>No Prompt</option> <option value="" selected>No Prompt</option>
<option value="normal">Assistant</option> <option value="normal">Assistant</option>
@@ -50,6 +50,19 @@
<label for="temperature" title="Temperature (0 - 1)"></label> <label for="temperature" title="Temperature (0 - 1)"></label>
<input id="temperature" type="number" min="0" max="1" step="0.05" value="0.85" /> <input id="temperature" type="number" min="0" max="1" step="0.05" value="0.85" />
</div> </div>
<div class="option none">
<label for="reasoning-effort" title="Reasoning Effort"></label>
<select id="reasoning-effort">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="hight">High</option>
<option value="">Custom</option>
</select>
</div>
<div class="option none">
<label for="reasoning-tokens" title="Maximum amount of reasoning tokens"></label>
<input id="reasoning-tokens" type="number" min="2" max="1" step="0.05" value="0.85" />
</div>
<div class="option"> <div class="option">
<button id="scrolling" title="Turn on auto-scrolling"></button> <button id="scrolling" title="Turn on auto-scrolling"></button>
</div> </div>

View File

@@ -7,12 +7,15 @@
$model = document.getElementById("model"), $model = document.getElementById("model"),
$prompt = document.getElementById("prompt"), $prompt = document.getElementById("prompt"),
$temperature = document.getElementById("temperature"), $temperature = document.getElementById("temperature"),
$reasoningEffort = document.getElementById("reasoning-effort"),
$reasoningTokens = document.getElementById("reasoning-tokens"),
$add = document.getElementById("add"), $add = document.getElementById("add"),
$send = document.getElementById("send"), $send = document.getElementById("send"),
$scrolling = document.getElementById("scrolling"), $scrolling = document.getElementById("scrolling"),
$clear = document.getElementById("clear"); $clear = document.getElementById("clear");
const messages = []; const messages = [],
models = {};
let autoScrolling = false, let autoScrolling = false,
interacted = false; interacted = false;
@@ -434,9 +437,9 @@
} }
async function loadModels() { async function loadModels() {
const models = await json("/-/models"); const modelList = await json("/-/models");
if (!models) { if (!modelList) {
alert("Failed to load models."); alert("Failed to load models.");
return []; return [];
@@ -444,7 +447,7 @@
$model.innerHTML = ""; $model.innerHTML = "";
for (const model of models) { for (const model of modelList) {
const el = document.createElement("option"); const el = document.createElement("option");
el.value = model.id; el.value = model.id;
@@ -454,18 +457,22 @@
el.dataset.tags = (model.tags || []).join(","); el.dataset.tags = (model.tags || []).join(",");
$model.appendChild(el); $model.appendChild(el);
models[model.id] = model;
} }
dropdown($model); dropdown($model);
return models; return modelList;
} }
function restore(models) { function restore(modelList) {
$role.value = loadValue("role", "user"); $role.value = loadValue("role", "user");
$model.value = loadValue("model", models[0].id); $model.value = loadValue("model", modelList[0].id);
$prompt.value = loadValue("prompt", "normal"); $prompt.value = loadValue("prompt", "normal");
$temperature.value = loadValue("temperature", 0.85); $temperature.value = loadValue("temperature", 0.85);
$reasoningEffort.value = loadValue("reasoning-effort", "medium");
$reasoningTokens.value = loadValue("reasoning-tokens", 1024);
if (loadValue("scrolling")) { if (loadValue("scrolling")) {
$scrolling.click(); $scrolling.click();
@@ -512,7 +519,21 @@
}); });
$model.addEventListener("change", () => { $model.addEventListener("change", () => {
storeValue("model", $model.value); const model = $model.value,
data = model ? models[model] : null;
storeValue("model", model);
if (data?.tags.includes("reasoning")) {
$reasoningEffort.parentNode.classList.remove("none");
$reasoningTokens.parentNode.classList.toggle(
"none",
!!$reasoningEffort.value,
);
} else {
$reasoningEffort.parentNode.classList.add("none");
$reasoningTokens.parentNode.classList.add("none");
}
}); });
$prompt.addEventListener("change", () => { $prompt.addEventListener("change", () => {
@@ -523,6 +544,18 @@
storeValue("temperature", $temperature.value); storeValue("temperature", $temperature.value);
}); });
$reasoningEffort.addEventListener("change", () => {
const effort = $reasoningEffort.value;
storeValue("reasoning-effort", effort);
$reasoningTokens.parentNode.classList.toggle("none", !!effort);
});
$reasoningTokens.addEventListener("change", () => {
storeValue("reasoning-tokens", $reasoningTokens.value);
});
$message.addEventListener("input", () => { $message.addEventListener("input", () => {
storeValue("message", $message.value); storeValue("message", $message.value);
}); });
@@ -570,12 +603,26 @@
return; return;
} }
if (!$temperature.value) {
$temperature.value = 0.85;
}
const temperature = parseFloat($temperature.value); const temperature = parseFloat($temperature.value);
if (Number.isNaN(temperature) || temperature < 0 || temperature > 1) { if (Number.isNaN(temperature) || temperature < 0 || temperature > 1) {
return; return;
} }
const effort = $reasoningEffort.value,
tokens = parseInt($reasoningTokens.value);
if (
!effort &&
(Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024)
) {
return;
}
pushMessage(); pushMessage();
controller = new AbortController(); controller = new AbortController();
@@ -586,6 +633,10 @@
prompt: $prompt.value, prompt: $prompt.value,
model: $model.value, model: $model.value,
temperature: temperature, temperature: temperature,
reasoning: {
effort: effort,
tokens: tokens || 0,
},
messages: messages.map((message) => message.getData()), messages: messages.map((message) => message.getData()),
}; };
@@ -644,6 +695,7 @@
dropdown($role); dropdown($role);
dropdown($prompt); dropdown($prompt);
dropdown($reasoningEffort);
loadModels().then(restore); loadModels().then(restore);
})(); })();