fix: cover extra thinking tags (#688) (thanks @theglove44)

This commit is contained in:
Peter Steinberger
2026-01-10 23:23:23 +01:00
parent a580639abf
commit 4d0e74ab6c
5 changed files with 30 additions and 17 deletions

View File

@@ -13,6 +13,7 @@
- Docker: allow optional home volume + extra bind mounts in `docker-setup.sh`. (#679) — thanks @gabriel-trigo.
### Fixes
- Agents: strip `<thought>`/`<antthinking>` tags from hidden reasoning output and cover tag variants in tests. (#688) — thanks @theglove44.
- Agents: recognize "usage limit" errors as rate limits for failover. (#687) — thanks @evalexpr.
- CLI: avoid success message when daemon restart is skipped. (#685) — thanks @carlulsoe.
- Gateway: disable the OpenAI-compatible `/v1/chat/completions` endpoint by default; enable via `gateway.http.endpoints.chatCompletions.enabled=true`.

View File

@@ -14,9 +14,7 @@ describe("gateway tool", () => {
vi.useFakeTimers();
const kill = vi.spyOn(process, "kill").mockImplementation(() => true);
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const stateDir = await fs.mkdtemp(
path.join(os.tmpdir(), "clawdbot-test-"),
);
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-test-"));
process.env.CLAWDBOT_STATE_DIR = stateDir;
try {

View File

@@ -10,6 +10,12 @@ type StubSession = {
type SessionEventHandler = (evt: unknown) => void;
describe("subscribeEmbeddedPiSession", () => {
const THINKING_TAG_CASES = [
{ tag: "think", open: "<think>", close: "</think>" },
{ tag: "thinking", open: "<thinking>", close: "</thinking>" },
{ tag: "thought", open: "<thought>", close: "</thought>" },
{ tag: "antthinking", open: "<antthinking>", close: "</antthinking>" },
] as const;
it("filters to <final> and falls back when tags are malformed", () => {
let handler: ((evt: unknown) => void) | undefined;
const session: StubSession = {
@@ -167,7 +173,12 @@ describe("subscribeEmbeddedPiSession", () => {
expect(onBlockReply.mock.calls[1][0].text).toBe("Final answer");
});
it("promotes <think> tags to thinking blocks at write-time", () => {
it.each(
THINKING_TAG_CASES,
)("promotes <%s> tags to thinking blocks at write-time", ({
open,
close,
}) => {
let handler: ((evt: unknown) => void) | undefined;
const session: StubSession = {
subscribe: (fn) => {
@@ -193,7 +204,7 @@ describe("subscribeEmbeddedPiSession", () => {
content: [
{
type: "text",
text: "<think>\nBecause it helps\n</think>\n\nFinal answer",
text: `${open}\nBecause it helps\n${close}\n\nFinal answer`,
},
],
} as AssistantMessage;
@@ -212,7 +223,12 @@ describe("subscribeEmbeddedPiSession", () => {
]);
});
it("streams <think> reasoning via onReasoningStream without leaking into final text", () => {
it.each(
THINKING_TAG_CASES,
)("streams <%s> reasoning via onReasoningStream without leaking into final text", ({
open,
close,
}) => {
let handler: ((evt: unknown) => void) | undefined;
const session: StubSession = {
subscribe: (fn) => {
@@ -240,7 +256,7 @@ describe("subscribeEmbeddedPiSession", () => {
message: { role: "assistant" },
assistantMessageEvent: {
type: "text_delta",
delta: "<think>\nBecause",
delta: `${open}\nBecause`,
},
});
@@ -249,7 +265,7 @@ describe("subscribeEmbeddedPiSession", () => {
message: { role: "assistant" },
assistantMessageEvent: {
type: "text_delta",
delta: " it helps\n</think>\n\nFinal answer",
delta: ` it helps\n${close}\n\nFinal answer`,
},
});
@@ -258,7 +274,7 @@ describe("subscribeEmbeddedPiSession", () => {
content: [
{
type: "text",
text: "<think>\nBecause it helps\n</think>\n\nFinal answer",
text: `${open}\nBecause it helps\n${close}\n\nFinal answer`,
},
],
} as AssistantMessage;
@@ -279,10 +295,9 @@ describe("subscribeEmbeddedPiSession", () => {
]);
});
it.each([
{ tag: "think", open: "<think>", close: "</think>" },
{ tag: "thinking", open: "<thinking>", close: "</thinking>" },
])("suppresses <%s> blocks across chunk boundaries", ({ open, close }) => {
it.each(
THINKING_TAG_CASES,
)("suppresses <%s> blocks across chunk boundaries", ({ open, close }) => {
let handler: ((evt: unknown) => void) | undefined;
const session: StubSession = {
subscribe: (fn) => {

View File

@@ -35,7 +35,8 @@ import {
const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
const THINKING_OPEN_RE = /<\s*(?:think(?:ing)?|thought|antthinking)\s*>/i;
const THINKING_CLOSE_RE = /<\s*\/\s*(?:think(?:ing)?|thought|antthinking)\s*>/i;
const THINKING_TAG_SCAN_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
const THINKING_TAG_SCAN_RE =
/<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
const TOOL_RESULT_MAX_CHARS = 8000;
const log = createSubsystemLogger("agent/embedded");
const RAW_STREAM_ENABLED = process.env.CLAWDBOT_RAW_STREAM === "1";

View File

@@ -165,9 +165,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
try {
await doctorCommand(defaultRuntime, { nonInteractive: true });
} catch (err) {
defaultRuntime.log(
theme.warn(`Doctor failed: ${String(err)}`),
);
defaultRuntime.log(theme.warn(`Doctor failed: ${String(err)}`));
} finally {
delete process.env.CLAWDBOT_UPDATE_IN_PROGRESS;
}