feat: enhance reasoning pattern handling in client configuration and UI

This commit is contained in:
vegu-ai-tools
2026-04-18 13:23:41 +03:00
parent da21d181e8
commit 71e47173fd
5 changed files with 63 additions and 12 deletions

View File

@@ -20,6 +20,7 @@
- "Message Regeneration: Fixed a bug where regenerating a message that fails (e.g., missing agent or function) would permanently remove the original message from the scene. The original message is now restored to both history and UI on failure, and unhandled exceptions during regeneration are caught."
- "Anthropic Client: Fixed generation error caused by missing required max_tokens parameter when response length capping is disabled. Now defaults to the model's API output token limit."
- "Anthropic Client: Cleaned up supported models list, removing outdated dated model IDs."
- "Reasoning Pattern: Fixed client reasoning pattern round-tripping through the default fallback. Blanking the pattern in the client config no longer silently re-saves as `.*?</think>`, and the template-derived pattern (e.g. Gemma 4's `<channel|>`) is now correctly adopted when the stored pattern is empty. The Client Config placeholder also shows the actual template-derived default instead of a hard-coded value."
- Fixed crash in voice library character manager when a character was deactivated.
- Fixed blank page on some Windows 10 systems where JavaScript files were served with incorrect MIME type (text/plain), causing Chromium-based browsers to reject module scripts.
- Fixed "No episodes available" placeholder being selectable in the episode list.

View File

@@ -708,31 +708,34 @@ class ClientBase:
if (
spec.reasoning_pattern
and spec.reasoning_pattern != self.reason_response_pattern
and spec.reasoning_pattern != self.client_config.reason_response_pattern
):
log.info("reasoning pattern determined from prompt template", spec=spec)
self.client_config.reason_response_pattern = spec.reasoning_pattern
return prompt
def prompt_template_example(self):
def prompt_template_example(self) -> tuple[str | None, str | None, PromptSpec]:
if not getattr(self, "model_name", None):
return None, None
return None, None, PromptSpec()
if not self.enabled:
return None, None
return None, None, PromptSpec()
model_name = self.model_name
if self.lock_template:
model_name = locked_model_template(self.name, self.model_name)
return model_prompt(
spec = PromptSpec()
rendered, template_file = model_prompt(
model_name,
"{sysmsg}",
"{prompt}<|BOT|>{LLM coercion}",
default_template=self.default_prompt_template,
reasoning_tokens=self.validated_reason_tokens if self.reason_enabled else 0,
spec=spec,
)
return rendered, template_file, spec
def split_prompt_for_coercion(self, prompt: str) -> tuple[str, str]:
"""
@@ -878,7 +881,9 @@ class ClientBase:
default_prompt_template = self.default_prompt_template
prompt_template_example, prompt_template_file = self.prompt_template_example()
prompt_template_example, prompt_template_file, prompt_template_spec = (
self.prompt_template_example()
)
has_prompt_template = (
prompt_template_file and prompt_template_file != default_prompt_template
)
@@ -898,7 +903,7 @@ class ClientBase:
log.debug("auto_determine_prompt_template", model_name=self.model_name)
self.auto_determine_prompt_template_attempt = self.model_name
self.determine_prompt_template()
prompt_template_example, prompt_template_file = (
prompt_template_example, prompt_template_file, prompt_template_spec = (
self.prompt_template_example()
)
has_prompt_template = (
@@ -913,6 +918,9 @@ class ClientBase:
"has_prompt_template": has_prompt_template,
"dedicated_default_template": dedicated_default_template,
"template_file": prompt_template_file,
"reason_response_pattern_default": (
prompt_template_spec.reasoning_pattern or DEFAULT_REASONING_PATTERN
),
"meta": self.Meta().model_dump(),
"error_action": None,
"double_coercion": self.double_coercion,
@@ -964,7 +972,7 @@ class ClientBase:
"reason_enabled": self.reason_enabled,
"reason_tokens": self.reason_tokens,
"min_reason_tokens": self.min_reason_tokens,
"reason_response_pattern": self.reason_response_pattern,
"reason_response_pattern": self.client_config.reason_response_pattern,
"reason_prefill": self.reason_prefill,
"reason_failure_behavior": self.reason_failure_behavior,
"requires_reasoning_pattern": self.requires_reasoning_pattern,

View File

@@ -6,9 +6,13 @@ from pathlib import Path
from talemate import VERSION
from talemate.changelog import list_revision_entries, delete_changelog_files
from talemate.client.model_prompts import model_prompt
from talemate.client.model_prompts import model_prompt, PromptSpec
from talemate.client.registry import CLIENT_CLASSES
from talemate.client.base import ClientBase, locked_model_template
from talemate.client.base import (
ClientBase,
DEFAULT_REASONING_PATTERN,
locked_model_template,
)
from talemate.config import Config as AppConfigData
from talemate.config.schema import GamePlayerCharacter
from talemate.config import get_config, Config, update_config
@@ -219,8 +223,23 @@ class ConfigPlugin(Plugin):
client_name=payload.client_name,
)
spec = PromptSpec()
client = None
if payload.client_name:
try:
client = get_client(payload.client_name)
except KeyError:
client = None
reasoning_tokens = 0
if client is not None and getattr(client, "reason_enabled", False):
reasoning_tokens = getattr(client, "validated_reason_tokens", 0) or 0
prompt_template_example, prompt_template_file = model_prompt(
model_name, "sysmsg", "prompt<|BOT|>{LLM coercion}"
model_name,
"sysmsg",
"prompt<|BOT|>{LLM coercion}",
reasoning_tokens=reasoning_tokens,
spec=spec,
)
log.info(
@@ -237,6 +256,9 @@ class ConfigPlugin(Plugin):
"prompt_template_example": prompt_template_example,
"has_prompt_template": True if prompt_template_example else False,
"template_file": prompt_template_file,
"reason_response_pattern_default": (
spec.reasoning_pattern or DEFAULT_REASONING_PATTERN
),
},
}
)

View File

@@ -206,12 +206,14 @@
<v-btn @click.stop="client.reason_response_pattern=''" size="small" color="primary" variant="text">{{ 'Default' }}</v-btn>
<!-- gpt-oss -->
<v-btn @click.stop="client.reason_response_pattern='.*?final<\\|message\\|>'" size="small" color="primary" variant="text">{{ 'gpt-oss' }}</v-btn>
<!-- gemma-4 -->
<v-btn @click.stop="client.reason_response_pattern='.*?<channel\\|>'" size="small" color="primary" variant="text">{{ 'gemma-4' }}</v-btn>
<!-- /think -->
<v-btn @click.stop="client.reason_response_pattern='.*?◁/think▷'" size="small" color="primary" variant="text">{{ '.*?◁/think▷' }}</v-btn>
<!-- </think> -->
<v-btn @click.stop="client.reason_response_pattern='.*?</think>'" size="small" color="primary" variant="text">{{ '.*?</think>' }}</v-btn>
</v-sheet>
<v-text-field v-model="client.reason_response_pattern" label="Pattern to strip from the response if the model is reasoning" hint="This is a regular expression that will be used to strip out the thinking tokens from the response." placeholder=".*?</think>"></v-text-field>
<v-text-field v-model="client.reason_response_pattern" label="Pattern to strip from the response if the model is reasoning" hint="This is a regular expression that will be used to strip out the thinking tokens from the response." :placeholder="client.data && client.data.reason_response_pattern_default ? client.data.reason_response_pattern_default : '.*?</think>'"></v-text-field>
</v-col>
</v-row>
<v-row v-if="client.reason_enabled && client.requires_reasoning_pattern">
@@ -705,6 +707,9 @@ export default {
this.client.data.has_prompt_template = data.data.has_prompt_template;
this.client.data.prompt_template_example = data.data.prompt_template_example;
this.client.data.template_file = data.data.template_file;
if (data.data.reason_response_pattern_default !== undefined) {
this.client.data.reason_response_pattern_default = data.data.reason_response_pattern_default;
}
this.waitingForTemplateSelection = false;
} else if (data.type === 'config' && data.action === 'std_llm_templates') {
console.log("Got std templates", data.data.templates);

View File

@@ -767,3 +767,18 @@ class TestPromptSpec:
)
assert spec.reasoning_pattern is not None
assert "seed:think" in spec.reasoning_pattern
def test_spec_reasoning_pattern_gpt_oss(self, model_prompt_with_std):
"""GPT-OSS template always sets reasoning_pattern."""
mp, _ = model_prompt_with_std
spec = PromptSpec()
mp(
model_name="__spec_test__",
system_message="sys",
prompt="user<|BOT|>",
default_template="GPT-OSS.jinja2",
spec=spec,
)
assert spec.reasoning_pattern is not None
assert "final" in spec.reasoning_pattern
assert "message" in spec.reasoning_pattern