feat(ai/dialect): expanded supported thinking delimiters for in-band leakage
- Added support for `<scratchpad>`, fenced markdown blocks, and specific model-specific channel tags to the thinking scan logic. - Exposed the `ThinkingInbandScanner` via the package dialect index.
This commit is contained in:
@@ -4,11 +4,14 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Enhanced cross-model reasoning recovery to support additional thinking dialects and leakage patterns
|
||||
|
||||
- Demote cross-vendor reasoning to plain text when the target does not natively support it
|
||||
- Refine cross-model reasoning preservation to prevent leaking inert context into structured fields
|
||||
- Rendered demoted cross-model reasoning blocks in the target model's canonical thinking dialect
|
||||
- Improved reliability of AI model responses by implementing automatic retry logic for detected thinking-loop stalls
|
||||
- Changed cross-provider/cross-model thinking demotion to render the prior turn's reasoning in the target model's canonical inline thinking dialect (a ```` ```thinking ```` fence for Gemini, `<think>`/`<thinking>` tags for others) instead of bare prose, with a neutral `<think>` fallback for control-token dialects (Harmony, Gemma) so chat-template tokens never leak into history. Replaying it as a native `thought` block was ruled out: end-to-end testing against Gemini 3 confirmed an unsigned `thought` part is schema-accepted but silently discarded — neither recalled nor influencing generation.
|
||||
- Broadened the leaked-thinking stream healer (`StreamMarkupHealing`'s `thinking` pattern) to recover reasoning emitted in any dialect's canonical idiom — Gemini's ` ```thinking ` fence, Gemma's `<|channel>thought` channel, Harmony's `analysis` message, and `<scratchpad>` — not just `<think>`/`<thinking>` tags, so leaked chain-of-thought is routed to thinking events for every dialect instead of rendered as raw markup
|
||||
|
||||
### Removed
|
||||
|
||||
|
||||
@@ -11,4 +11,5 @@ export * from "./owned-stream";
|
||||
// consumer needs (the legacy markdown `/dump` reuses its `<thinking>` envelope
|
||||
// unwrap), so re-export only that symbol rather than `export *`-ing the rest.
|
||||
export { renderDelimitedThinking } from "./rendering";
|
||||
export * from "./thinking";
|
||||
export * from "./types";
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import { partialSuffixOverlapAny } from "./coercion";
|
||||
import type { InbandScanEvent, InbandScanner } from "./types";
|
||||
|
||||
const THINK_OPEN = "<think>";
|
||||
const THINK_CLOSE = "</think>";
|
||||
const THINKING_OPEN = "<thinking>";
|
||||
const THINKING_CLOSE = "</thinking>";
|
||||
const TAGS = [
|
||||
{ open: THINK_OPEN, close: THINK_CLOSE },
|
||||
{ open: THINKING_OPEN, close: THINKING_CLOSE },
|
||||
] as const;
|
||||
const OPENS = [THINK_OPEN, THINKING_OPEN] as const;
|
||||
|
||||
type Tag = { readonly open: string; readonly close: string };
|
||||
|
||||
/**
|
||||
* Every dialect's in-band thinking section in its canonical `renderThinking`
|
||||
* form (see the sibling `./*.ts` scanners). {@link ThinkingInbandScanner} heals
|
||||
* reasoning a model leaked into its visible text channel back into thinking
|
||||
* events, whichever dialect idiom the leak used.
|
||||
*
|
||||
* Plain (attribute-free) delimiters only — matching what `renderThinking`
|
||||
* emits and what models leak in practice. Attributed or namespaced XML thinking
|
||||
* tags (`<thinking signature="…">`, `antml:thinking`) are recovered by the owned
|
||||
* anthropic-dialect parser, not this text-channel healing fallback.
|
||||
*/
|
||||
const TAGS: readonly Tag[] = [
|
||||
{ open: "<think>", close: "</think>" }, // deepseek, glm, hermes, kimi, qwen3 (and anthropic/minimax/xml)
|
||||
{ open: "<thinking>", close: "</thinking>" }, // anthropic, minimax, xml
|
||||
{ open: "<scratchpad>", close: "</scratchpad>" }, // anthropic
|
||||
{ open: "```thinking\n", close: "```" }, // gemini fenced thinking
|
||||
{ open: "<|channel>thought\n", close: "<channel|>" }, // gemma reasoning channel
|
||||
{ open: "<|start|>assistant<|channel|>analysis<|message|>", close: "<|end|>" }, // harmony analysis (rendered)
|
||||
{ open: "<|channel|>analysis<|message|>", close: "<|end|>" }, // harmony analysis (bare leak)
|
||||
];
|
||||
const OPENS = TAGS.map(tag => tag.open);
|
||||
|
||||
export class ThinkingInbandScanner implements InbandScanner {
|
||||
#buffer = "";
|
||||
#closeTag = "";
|
||||
|
||||
@@ -164,7 +164,6 @@ describe("kimi think tags (<think>…</think>)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("every dialect round-trips thinking (no missing thinking element)", () => {
|
||||
const dialects: Dialect[] = [
|
||||
"anthropic",
|
||||
|
||||
@@ -31,7 +31,6 @@ describe("renderToolExamples", () => {
|
||||
expect(rendered).toContain("</examples>");
|
||||
});
|
||||
|
||||
|
||||
it("renders call example in hermes format", () => {
|
||||
const tool: InbandTool = {
|
||||
name: "find",
|
||||
|
||||
Reference in New Issue
Block a user